Initial commit.
This commit is contained in:
1127
src/CMakeLists.txt
Normal file
1127
src/CMakeLists.txt
Normal file
File diff suppressed because it is too large
Load Diff
14
src/analyzer/analyzer.cpp
Normal file
14
src/analyzer/analyzer.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "analyzer.h"
|
||||
|
||||
#include "engines/enginebase.h"
|
||||
|
||||
AnalyzerBase::AnalyzerBase(QWidget* parent)
|
||||
: QGLWidget(parent), engine_(nullptr) {}
|
||||
|
||||
void AnalyzerBase::set_engine(Engine::Base* engine) {
|
||||
disconnect(engine_);
|
||||
engine_ = engine;
|
||||
if (engine_) {
|
||||
connect(engine_, SIGNAL(SpectrumAvailable(const QVector<float>&)), SLOT(SpectrumAvailable(const QVector<float>&)));
|
||||
}
|
||||
}
|
||||
223
src/analyzer/analyzerbase.cpp
Executable file
223
src/analyzer/analyzerbase.cpp
Executable file
@@ -0,0 +1,223 @@
|
||||
/***************************************************************************
|
||||
viswidget.cpp - description
|
||||
-------------------
|
||||
begin : Die Jan 7 2003
|
||||
copyright : (C) 2003 by Max Howell
|
||||
email : markey@web.de
|
||||
***************************************************************************/
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* This program 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 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#include "analyzerbase.h"
|
||||
|
||||
#include <cmath> //interpolate()
|
||||
|
||||
#include <QEvent> //event()
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "engine/enginebase.h"
|
||||
|
||||
// INSTRUCTIONS Base2D
|
||||
// 1. do anything that depends on height() in init(), Base2D will call it before
|
||||
// you are shown
|
||||
// 2. otherwise you can use the constructor to initialise things
|
||||
// 3. reimplement analyze(), and paint to canvas(), Base2D will update the
|
||||
// widget when you return control to it
|
||||
// 4. if you want to manipulate the scope, reimplement transform()
|
||||
// 5. for convenience <vector> <qpixmap.h> <qwdiget.h> are pre-included
|
||||
// TODO make an INSTRUCTIONS file
|
||||
// can't mod scope in analyze you have to use transform
|
||||
|
||||
// TODO for 2D use setErasePixmap Qt function insetead of m_background
|
||||
|
||||
// make the linker happy only for gcc < 4.0
|
||||
#if !(__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 0)) && \
|
||||
!defined(Q_OS_WIN32)
|
||||
template class Analyzer::Base<QWidget>;
|
||||
#endif
|
||||
|
||||
Analyzer::Base::Base(QWidget* parent, uint scopeSize)
|
||||
: QWidget(parent),
|
||||
m_timeout(40) // msec
|
||||
,
|
||||
m_fht(new FHT(scopeSize)),
|
||||
m_engine(nullptr),
|
||||
m_lastScope(512),
|
||||
current_chunk_(0),
|
||||
new_frame_(false),
|
||||
is_playing_(false) {}
|
||||
|
||||
void Analyzer::Base::hideEvent(QHideEvent*) { m_timer.stop(); }
|
||||
|
||||
void Analyzer::Base::showEvent(QShowEvent*) { m_timer.start(timeout(), this); }
|
||||
|
||||
void Analyzer::Base::transform(Scope& scope) // virtual
|
||||
{
|
||||
|
||||
// this is a standard transformation that should give
|
||||
// an FFT scope that has bands for pretty analyzers
|
||||
|
||||
// NOTE resizing here is redundant as FHT routines only calculate FHT::size()
|
||||
// values
|
||||
// scope.resize( m_fht->size() );
|
||||
|
||||
float* front = static_cast<float*>(&scope.front());
|
||||
|
||||
float* f = new float[m_fht->size()];
|
||||
m_fht->copy(&f[0], front);
|
||||
m_fht->logSpectrum(front, &f[0]);
|
||||
m_fht->scale(front, 1.0 / 20);
|
||||
|
||||
scope.resize(m_fht->size() / 2); // second half of values are rubbish
|
||||
delete[] f;
|
||||
|
||||
}
|
||||
|
||||
void Analyzer::Base::paintEvent(QPaintEvent* e) {
|
||||
|
||||
QPainter p(this);
|
||||
p.fillRect(e->rect(), palette().color(QPalette::Window));
|
||||
|
||||
switch (m_engine->state()) {
|
||||
case Engine::Playing: {
|
||||
const Engine::Scope& thescope = m_engine->scope(m_timeout);
|
||||
int i = 0;
|
||||
|
||||
// convert to mono here - our built in analyzers need mono, but the
|
||||
// engines provide interleaved pcm
|
||||
for (uint x = 0; (int)x < m_fht->size(); ++x) {
|
||||
m_lastScope[x] = double(thescope[i] + thescope[i + 1]) / (2 * (1 << 15));
|
||||
i += 2;
|
||||
}
|
||||
|
||||
is_playing_ = true;
|
||||
transform(m_lastScope);
|
||||
analyze(p, m_lastScope, new_frame_);
|
||||
|
||||
// scope.resize( m_fht->size() );
|
||||
|
||||
break;
|
||||
}
|
||||
case Engine::Paused:
|
||||
is_playing_ = false;
|
||||
analyze(p, m_lastScope, new_frame_);
|
||||
break;
|
||||
|
||||
default:
|
||||
is_playing_ = false;
|
||||
demo(p);
|
||||
}
|
||||
|
||||
new_frame_ = false;
|
||||
|
||||
}
|
||||
|
||||
int Analyzer::Base::resizeExponent(int exp) {
|
||||
if (exp < 3)
|
||||
exp = 3;
|
||||
else if (exp > 9)
|
||||
exp = 9;
|
||||
|
||||
if (exp != m_fht->sizeExp()) {
|
||||
delete m_fht;
|
||||
m_fht = new FHT(exp);
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
int Analyzer::Base::resizeForBands(int bands) {
|
||||
|
||||
int exp;
|
||||
if (bands <= 8)
|
||||
exp = 4;
|
||||
else if (bands <= 16)
|
||||
exp = 5;
|
||||
else if (bands <= 32)
|
||||
exp = 6;
|
||||
else if (bands <= 64)
|
||||
exp = 7;
|
||||
else if (bands <= 128)
|
||||
exp = 8;
|
||||
else
|
||||
exp = 9;
|
||||
|
||||
resizeExponent(exp);
|
||||
return m_fht->size() / 2;
|
||||
|
||||
}
|
||||
|
||||
void Analyzer::Base::demo(QPainter& p) // virtual
|
||||
{
|
||||
|
||||
static int t = 201; // FIXME make static to namespace perhaps
|
||||
|
||||
if (t > 999) t = 1; // 0 = wasted calculations
|
||||
if (t < 201) {
|
||||
Scope s(32);
|
||||
|
||||
const double dt = double(t) / 200;
|
||||
for (uint i = 0; i < s.size(); ++i)
|
||||
s[i] = dt * (sin(M_PI + (i * M_PI) / s.size()) + 1.0);
|
||||
|
||||
analyze(p, s, new_frame_);
|
||||
} else
|
||||
analyze(p, Scope(32, 0), new_frame_);
|
||||
|
||||
++t;
|
||||
|
||||
}
|
||||
|
||||
void Analyzer::Base::polishEvent() {
|
||||
init(); // virtual
|
||||
}
|
||||
|
||||
void Analyzer::interpolate(const Scope& inVec, Scope& outVec) // static
|
||||
{
|
||||
|
||||
double pos = 0.0;
|
||||
const double step = (double)inVec.size() / outVec.size();
|
||||
|
||||
for (uint i = 0; i < outVec.size(); ++i, pos += step) {
|
||||
const double error = pos - std::floor(pos);
|
||||
const unsigned long offset = (unsigned long)pos;
|
||||
|
||||
unsigned long indexLeft = offset + 0;
|
||||
|
||||
if (indexLeft >= inVec.size()) indexLeft = inVec.size() - 1;
|
||||
|
||||
unsigned long indexRight = offset + 1;
|
||||
|
||||
if (indexRight >= inVec.size()) indexRight = inVec.size() - 1;
|
||||
|
||||
outVec[i] = inVec[indexLeft] * (1.0 - error) + inVec[indexRight] * error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Analyzer::initSin(Scope& v, const uint size) // static
|
||||
{
|
||||
double step = (M_PI * 2) / size;
|
||||
double radian = 0;
|
||||
|
||||
for (uint i = 0; i < size; i++) {
|
||||
v.push_back(sin(radian));
|
||||
radian += step;
|
||||
}
|
||||
}
|
||||
|
||||
void Analyzer::Base::timerEvent(QTimerEvent* e) {
|
||||
QWidget::timerEvent(e);
|
||||
if (e->timerId() != m_timer.timerId()) return;
|
||||
|
||||
new_frame_ = true;
|
||||
update();
|
||||
}
|
||||
89
src/analyzer/analyzerbase.h
Normal file
89
src/analyzer/analyzerbase.h
Normal file
@@ -0,0 +1,89 @@
|
||||
// Maintainer: Max Howell <max.howell@methylblue.com>, (C) 2004
|
||||
// Copyright: See COPYING file that comes with this distribution
|
||||
|
||||
#ifndef ANALYZERBASE_H
|
||||
#define ANALYZERBASE_H
|
||||
|
||||
#ifdef __FreeBSD__
|
||||
#include <sys/types.h>
|
||||
#endif
|
||||
|
||||
#include "analyzer/fht.h" //stack allocated and convenience
|
||||
#include "engine/engine_fwd.h"
|
||||
#include <QPixmap> //stack allocated and convenience
|
||||
#include <QBasicTimer> //stack allocated
|
||||
#include <QWidget> //baseclass
|
||||
#include <vector> //included for convenience
|
||||
|
||||
#include <QGLWidget> //baseclass
|
||||
#ifdef Q_WS_MACX
|
||||
#include <OpenGL/gl.h> //included for convenience
|
||||
#include <OpenGL/glu.h> //included for convenience
|
||||
#else
|
||||
#include <GL/gl.h> //included for convenience
|
||||
#include <GL/glu.h> //included for convenience
|
||||
#endif
|
||||
|
||||
class QEvent;
|
||||
class QPaintEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
namespace Analyzer {
|
||||
|
||||
typedef std::vector<float> Scope;
|
||||
|
||||
class Base : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
~Base() { delete m_fht; }
|
||||
|
||||
uint timeout() const { return m_timeout; }
|
||||
|
||||
void set_engine(EngineBase *engine) { m_engine = engine; }
|
||||
|
||||
void changeTimeout(uint newTimeout) {
|
||||
m_timeout = newTimeout;
|
||||
if (m_timer.isActive()) {
|
||||
m_timer.stop();
|
||||
m_timer.start(m_timeout, this);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void framerateChanged() {}
|
||||
|
||||
protected:
|
||||
Base(QWidget*, uint scopeSize = 7);
|
||||
|
||||
void hideEvent(QHideEvent*);
|
||||
void showEvent(QShowEvent*);
|
||||
void paintEvent(QPaintEvent*);
|
||||
void timerEvent(QTimerEvent*);
|
||||
|
||||
void polishEvent();
|
||||
|
||||
int resizeExponent(int);
|
||||
int resizeForBands(int);
|
||||
virtual void init() {}
|
||||
virtual void transform(Scope&);
|
||||
virtual void analyze(QPainter& p, const Scope&, bool new_frame) = 0;
|
||||
virtual void demo(QPainter& p);
|
||||
|
||||
protected:
|
||||
QBasicTimer m_timer;
|
||||
uint m_timeout;
|
||||
FHT* m_fht;
|
||||
EngineBase* m_engine;
|
||||
Scope m_lastScope;
|
||||
int current_chunk_;
|
||||
|
||||
bool new_frame_;
|
||||
bool is_playing_;
|
||||
};
|
||||
|
||||
void interpolate(const Scope&, Scope&);
|
||||
void initSin(Scope&, const uint = 6000);
|
||||
|
||||
} // END namespace Analyzer
|
||||
|
||||
#endif
|
||||
220
src/analyzer/analyzercontainer.cpp
Normal file
220
src/analyzer/analyzercontainer.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Strawberry Music Player
|
||||
This file was part of Clementine.
|
||||
Copyright 2010, 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 "analyzercontainer.h"
|
||||
#include "blockanalyzer.h"
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QtDebug>
|
||||
|
||||
const char* AnalyzerContainer::kSettingsGroup = "Analyzer";
|
||||
const char* AnalyzerContainer::kSettingsFramerate = "framerate";
|
||||
|
||||
// Framerates
|
||||
const int AnalyzerContainer::kLowFramerate = 20;
|
||||
const int AnalyzerContainer::kMediumFramerate = 25;
|
||||
const int AnalyzerContainer::kHighFramerate = 30;
|
||||
const int AnalyzerContainer::kSuperHighFramerate = 60;
|
||||
|
||||
AnalyzerContainer::AnalyzerContainer(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
current_framerate_(kMediumFramerate),
|
||||
context_menu_(new QMenu(this)),
|
||||
context_menu_framerate_(new QMenu(tr("Framerate"), this)),
|
||||
group_(new QActionGroup(this)),
|
||||
group_framerate_(new QActionGroup(this)),
|
||||
mapper_(new QSignalMapper(this)),
|
||||
mapper_framerate_(new QSignalMapper(this)),
|
||||
visualisation_action_(nullptr),
|
||||
double_click_timer_(new QTimer(this)),
|
||||
ignore_next_click_(false),
|
||||
current_analyzer_(nullptr),
|
||||
engine_(nullptr) {
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
setLayout(layout);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Init framerate sub-menu
|
||||
AddFramerate(tr("Low (%1 fps)").arg(kLowFramerate), kLowFramerate);
|
||||
AddFramerate(tr("Medium (%1 fps)").arg(kMediumFramerate), kMediumFramerate);
|
||||
AddFramerate(tr("High (%1 fps)").arg(kHighFramerate), kHighFramerate);
|
||||
AddFramerate(tr("Super high (%1 fps)").arg(kSuperHighFramerate), kSuperHighFramerate);
|
||||
connect(mapper_framerate_, SIGNAL(mapped(int)), SLOT(ChangeFramerate(int)));
|
||||
|
||||
context_menu_->addMenu(context_menu_framerate_);
|
||||
context_menu_->addSeparator();
|
||||
|
||||
AddAnalyzerType<BlockAnalyzer>();
|
||||
|
||||
connect(mapper_, SIGNAL(mapped(int)), SLOT(ChangeAnalyzer(int)));
|
||||
disable_action_ = context_menu_->addAction(tr("No analyzer"), this, SLOT(DisableAnalyzer()));
|
||||
disable_action_->setCheckable(true);
|
||||
group_->addAction(disable_action_);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
// Visualisation action gets added in SetActions
|
||||
|
||||
double_click_timer_->setSingleShot(true);
|
||||
double_click_timer_->setInterval(250);
|
||||
connect(double_click_timer_, SIGNAL(timeout()), SLOT(ShowPopupMenu()));
|
||||
|
||||
Load();
|
||||
}
|
||||
|
||||
void AnalyzerContainer::SetActions(QAction* visualisation) {
|
||||
visualisation_action_ = visualisation;
|
||||
context_menu_->addAction(visualisation_action_);
|
||||
}
|
||||
|
||||
void AnalyzerContainer::mouseReleaseEvent(QMouseEvent* e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if (ignore_next_click_) {
|
||||
ignore_next_click_ = false;
|
||||
}
|
||||
else {
|
||||
// Might be the first click in a double click, so wait a while before
|
||||
// actually doing anything
|
||||
double_click_timer_->start();
|
||||
last_click_pos_ = e->globalPos();
|
||||
}
|
||||
}
|
||||
else if (e->button() == Qt::RightButton) {
|
||||
context_menu_->popup(e->globalPos());
|
||||
}
|
||||
}
|
||||
|
||||
void AnalyzerContainer::ShowPopupMenu() {
|
||||
context_menu_->popup(last_click_pos_);
|
||||
}
|
||||
|
||||
void AnalyzerContainer::mouseDoubleClickEvent(QMouseEvent*) {
|
||||
double_click_timer_->stop();
|
||||
ignore_next_click_ = true;
|
||||
|
||||
if (visualisation_action_) visualisation_action_->trigger();
|
||||
}
|
||||
|
||||
void AnalyzerContainer::wheelEvent(QWheelEvent* e) {
|
||||
emit WheelEvent(e->delta());
|
||||
}
|
||||
|
||||
void AnalyzerContainer::SetEngine(EngineBase* engine) {
|
||||
if (current_analyzer_) current_analyzer_->set_engine(engine);
|
||||
engine_ = engine;
|
||||
}
|
||||
|
||||
void AnalyzerContainer::DisableAnalyzer() {
|
||||
delete current_analyzer_;
|
||||
current_analyzer_ = nullptr;
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
void AnalyzerContainer::ChangeAnalyzer(int id) {
|
||||
QObject* instance = analyzer_types_[id]->newInstance(Q_ARG(QWidget*, this));
|
||||
|
||||
if (!instance) {
|
||||
qLog(Warning) << "Couldn't intialise a new"
|
||||
<< analyzer_types_[id]->className();
|
||||
return;
|
||||
}
|
||||
|
||||
delete current_analyzer_;
|
||||
current_analyzer_ = qobject_cast<Analyzer::Base*>(instance);
|
||||
current_analyzer_->set_engine(engine_);
|
||||
// Even if it is not supposed to happen, I don't want to get a dbz error
|
||||
current_framerate_ = current_framerate_ == 0 ? kMediumFramerate : current_framerate_;
|
||||
current_analyzer_->changeTimeout(1000 / current_framerate_);
|
||||
|
||||
layout()->addWidget(current_analyzer_);
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
void AnalyzerContainer::ChangeFramerate(int new_framerate) {
|
||||
if (current_analyzer_) {
|
||||
// Even if it is not supposed to happen, I don't want to get a dbz error
|
||||
new_framerate = new_framerate == 0 ? kMediumFramerate : new_framerate;
|
||||
current_analyzer_->changeTimeout(1000 / new_framerate);
|
||||
|
||||
// notify the current analyzer that the framerate has changed
|
||||
current_analyzer_->framerateChanged();
|
||||
}
|
||||
SaveFramerate(new_framerate);
|
||||
}
|
||||
|
||||
void AnalyzerContainer::Load() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
// Analyzer
|
||||
QString type = s.value("type", "BlockAnalyzer").toString();
|
||||
if (type.isEmpty()) {
|
||||
DisableAnalyzer();
|
||||
disable_action_->setChecked(true);
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < analyzer_types_.count(); ++i) {
|
||||
if (type == analyzer_types_[i]->className()) {
|
||||
ChangeAnalyzer(i);
|
||||
actions_[i]->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Framerate
|
||||
current_framerate_ = s.value(kSettingsFramerate, kMediumFramerate).toInt();
|
||||
for (int i = 0; i < framerate_list_.count(); ++i) {
|
||||
if (current_framerate_ == framerate_list_[i]) {
|
||||
ChangeFramerate(current_framerate_);
|
||||
group_framerate_->actions()[i]->setChecked(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnalyzerContainer::SaveFramerate(int framerate) {
|
||||
// For now, framerate is common for all analyzers. Maybe each analyzer should
|
||||
// have its own framerate?
|
||||
current_framerate_ = framerate;
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue(kSettingsFramerate, current_framerate_);
|
||||
}
|
||||
|
||||
void AnalyzerContainer::Save() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
s.setValue("type", current_analyzer_ ? current_analyzer_->metaObject()->className() : QVariant());
|
||||
}
|
||||
|
||||
void AnalyzerContainer::AddFramerate(const QString& name, int framerate) {
|
||||
QAction* action = context_menu_framerate_->addAction(name, mapper_framerate_, SLOT(map()));
|
||||
mapper_framerate_->setMapping(action, framerate);
|
||||
group_framerate_->addAction(action);
|
||||
framerate_list_ << framerate;
|
||||
action->setCheckable(true);
|
||||
}
|
||||
106
src/analyzer/analyzercontainer.h
Normal file
106
src/analyzer/analyzercontainer.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Strawberry Music Player
|
||||
This file was part of Clementine.
|
||||
Copyright 2010, 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 ANALYZERCONTAINER_H
|
||||
#define ANALYZERCONTAINER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QMenu>
|
||||
#include <QSignalMapper>
|
||||
|
||||
#include "analyzerbase.h"
|
||||
#include "engine/engine_fwd.h"
|
||||
|
||||
class AnalyzerContainer : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AnalyzerContainer(QWidget* parent);
|
||||
|
||||
void SetEngine(EngineBase *engine);
|
||||
void SetActions(QAction *visualisation);
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kSettingsFramerate;
|
||||
|
||||
signals:
|
||||
void WheelEvent(int delta);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent*);
|
||||
void mouseDoubleClickEvent(QMouseEvent*);
|
||||
void wheelEvent(QWheelEvent *e);
|
||||
|
||||
private slots:
|
||||
void ChangeAnalyzer(int id);
|
||||
void ChangeFramerate(int new_framerate);
|
||||
void DisableAnalyzer();
|
||||
void ShowPopupMenu();
|
||||
|
||||
private:
|
||||
static const int kLowFramerate;
|
||||
static const int kMediumFramerate;
|
||||
static const int kHighFramerate;
|
||||
static const int kSuperHighFramerate;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
void SaveFramerate(int framerate);
|
||||
template <typename T>
|
||||
void AddAnalyzerType();
|
||||
void AddFramerate(const QString& name, int framerate);
|
||||
|
||||
private:
|
||||
int current_framerate_; // fps
|
||||
QMenu *context_menu_;
|
||||
QMenu *context_menu_framerate_;
|
||||
QActionGroup *group_;
|
||||
QActionGroup *group_framerate_;
|
||||
QSignalMapper *mapper_;
|
||||
QSignalMapper *mapper_framerate_;
|
||||
|
||||
QList<const QMetaObject*> analyzer_types_;
|
||||
QList<int> framerate_list_;
|
||||
QList<QAction*> actions_;
|
||||
QAction *disable_action_;
|
||||
|
||||
QAction *visualisation_action_;
|
||||
QTimer *double_click_timer_;
|
||||
QPoint last_click_pos_;
|
||||
bool ignore_next_click_;
|
||||
|
||||
Analyzer::Base* current_analyzer_;
|
||||
EngineBase *engine_;
|
||||
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void AnalyzerContainer::AddAnalyzerType() {
|
||||
int id = analyzer_types_.count();
|
||||
analyzer_types_ << &T::staticMetaObject;
|
||||
|
||||
QAction *action = context_menu_->addAction(tr(T::kName), mapper_, SLOT(map()));
|
||||
group_->addAction(action);
|
||||
mapper_->setMapping(action, id);
|
||||
action->setCheckable(true);
|
||||
actions_ << action;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
417
src/analyzer/blockanalyzer.cpp
Normal file
417
src/analyzer/blockanalyzer.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
// Author: Max Howell <max.howell@methylblue.com>, (C) 2003-5
|
||||
// Mark Kretschmann <markey@web.de>, (C) 2005
|
||||
// Copyright: See COPYING file that comes with this distribution
|
||||
//
|
||||
|
||||
#include "blockanalyzer.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <cstdlib>
|
||||
#include <QPainter>
|
||||
|
||||
const uint BlockAnalyzer::HEIGHT = 2;
|
||||
const uint BlockAnalyzer::WIDTH = 4;
|
||||
const uint BlockAnalyzer::MIN_ROWS = 3; // arbituary
|
||||
const uint BlockAnalyzer::MIN_COLUMNS = 32; // arbituary
|
||||
const uint BlockAnalyzer::MAX_COLUMNS = 256; // must be 2**n
|
||||
const uint BlockAnalyzer::FADE_SIZE = 90;
|
||||
|
||||
const char* BlockAnalyzer::kName =
|
||||
QT_TRANSLATE_NOOP("AnalyzerContainer", "Block analyzer");
|
||||
|
||||
BlockAnalyzer::BlockAnalyzer(QWidget* parent)
|
||||
: Analyzer::Base(parent, 9),
|
||||
m_columns(0) // uint
|
||||
,
|
||||
m_rows(0) // uint
|
||||
,
|
||||
m_y(0) // uint
|
||||
,
|
||||
m_barPixmap(1, 1) // null qpixmaps cause crashes
|
||||
,
|
||||
m_topBarPixmap(WIDTH, HEIGHT),
|
||||
m_scope(MIN_COLUMNS) // Scope
|
||||
,
|
||||
m_store(1 << 8, 0) // vector<uint>
|
||||
,
|
||||
m_fade_bars(FADE_SIZE) // vector<QPixmap>
|
||||
,
|
||||
m_fade_pos(1 << 8, 50) // vector<uint>
|
||||
,
|
||||
m_fade_intensity(1 << 8, 32) // vector<uint>
|
||||
{
|
||||
setMinimumSize(MIN_COLUMNS * (WIDTH + 1) - 1,
|
||||
MIN_ROWS * (HEIGHT + 1) -
|
||||
1); //-1 is padding, no drawing takes place there
|
||||
setMaximumWidth(MAX_COLUMNS * (WIDTH + 1) - 1);
|
||||
|
||||
// mxcl says null pixmaps cause crashes, so let's play it safe
|
||||
for (uint i = 0; i < FADE_SIZE; ++i) m_fade_bars[i] = QPixmap(1, 1);
|
||||
}
|
||||
|
||||
BlockAnalyzer::~BlockAnalyzer() {}
|
||||
|
||||
void BlockAnalyzer::resizeEvent(QResizeEvent* e) {
|
||||
QWidget::resizeEvent(e);
|
||||
|
||||
m_background = QPixmap(size());
|
||||
canvas_ = QPixmap(size());
|
||||
|
||||
const uint oldRows = m_rows;
|
||||
|
||||
// all is explained in analyze()..
|
||||
//+1 to counter -1 in maxSizes, trust me we need this!
|
||||
m_columns = qMax(uint(double(width() + 1) / (WIDTH + 1)), MAX_COLUMNS);
|
||||
m_rows = uint(double(height() + 1) / (HEIGHT + 1));
|
||||
|
||||
// this is the y-offset for drawing from the top of the widget
|
||||
m_y = (height() - (m_rows * (HEIGHT + 1)) + 2) / 2;
|
||||
|
||||
m_scope.resize(m_columns);
|
||||
|
||||
if (m_rows != oldRows) {
|
||||
m_barPixmap = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
|
||||
|
||||
for (uint i = 0; i < FADE_SIZE; ++i)
|
||||
m_fade_bars[i] = QPixmap(WIDTH, m_rows * (HEIGHT + 1));
|
||||
|
||||
m_yscale.resize(m_rows + 1);
|
||||
|
||||
const uint PRE = 1,
|
||||
PRO = 1; // PRE and PRO allow us to restrict the range somewhat
|
||||
|
||||
for (uint z = 0; z < m_rows; ++z)
|
||||
m_yscale[z] = 1 - (log10(PRE + z) / log10(PRE + m_rows + PRO));
|
||||
|
||||
m_yscale[m_rows] = 0;
|
||||
|
||||
determineStep();
|
||||
paletteChange(palette());
|
||||
}
|
||||
|
||||
drawBackground();
|
||||
}
|
||||
|
||||
void BlockAnalyzer::determineStep() {
|
||||
// falltime is dependent on rowcount due to our digital resolution (ie we have
|
||||
// boxes/blocks of pixels)
|
||||
// I calculated the value 30 based on some trial and error
|
||||
|
||||
// the fall time of 30 is too slow on framerates above 50fps
|
||||
const double fallTime = timeout() < 20 ? 20 * m_rows : 30 * m_rows;
|
||||
|
||||
m_step = double(m_rows * timeout()) / fallTime;
|
||||
}
|
||||
|
||||
void BlockAnalyzer::framerateChanged() { // virtual
|
||||
determineStep();
|
||||
}
|
||||
|
||||
void BlockAnalyzer::transform(Analyzer::Scope& s) // pure virtual
|
||||
{
|
||||
for (uint x = 0; x < s.size(); ++x) s[x] *= 2;
|
||||
|
||||
float* front = static_cast<float*>(&s.front());
|
||||
|
||||
m_fht->spectrum(front);
|
||||
m_fht->scale(front, 1.0 / 20);
|
||||
|
||||
// the second half is pretty dull, so only show it if the user has a large
|
||||
// analyzer
|
||||
// by setting to m_scope.size() if large we prevent interpolation of large
|
||||
// analyzers, this is good!
|
||||
s.resize(m_scope.size() <= MAX_COLUMNS / 2 ? MAX_COLUMNS / 2 : m_scope.size());
|
||||
}
|
||||
|
||||
void BlockAnalyzer::analyze(QPainter& p, const Analyzer::Scope& s,
|
||||
bool new_frame) {
|
||||
// y = 2 3 2 1 0 2
|
||||
// . . . . # .
|
||||
// . . . # # .
|
||||
// # . # # # #
|
||||
// # # # # # #
|
||||
//
|
||||
// visual aid for how this analyzer works.
|
||||
// y represents the number of blanks
|
||||
// y starts from the top and increases in units of blocks
|
||||
|
||||
// m_yscale looks similar to: { 0.7, 0.5, 0.25, 0.15, 0.1, 0 }
|
||||
// if it contains 6 elements there are 5 rows in the analyzer
|
||||
|
||||
if (!new_frame) {
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
return;
|
||||
}
|
||||
|
||||
QPainter canvas_painter(&canvas_);
|
||||
|
||||
Analyzer::interpolate(s, m_scope);
|
||||
|
||||
// Paint the background
|
||||
canvas_painter.drawPixmap(0, 0, m_background);
|
||||
|
||||
for (uint y, x = 0; x < m_scope.size(); ++x) {
|
||||
// determine y
|
||||
for (y = 0; m_scope[x] < m_yscale[y]; ++y)
|
||||
;
|
||||
|
||||
// this is opposite to what you'd think, higher than y
|
||||
// means the bar is lower than y (physically)
|
||||
if ((float)y > m_store[x])
|
||||
y = int(m_store[x] += m_step);
|
||||
else
|
||||
m_store[x] = y;
|
||||
|
||||
// if y is lower than m_fade_pos, then the bar has exceeded the height of
|
||||
// the fadeout
|
||||
// if the fadeout is quite faded now, then display the new one
|
||||
if (y <= m_fade_pos[x] /*|| m_fade_intensity[x] < FADE_SIZE / 3*/) {
|
||||
m_fade_pos[x] = y;
|
||||
m_fade_intensity[x] = FADE_SIZE;
|
||||
}
|
||||
|
||||
if (m_fade_intensity[x] > 0) {
|
||||
const uint offset = --m_fade_intensity[x];
|
||||
const uint y = m_y + (m_fade_pos[x] * (HEIGHT + 1));
|
||||
canvas_painter.drawPixmap(x * (WIDTH + 1), y, m_fade_bars[offset], 0, 0, WIDTH, height() - y);
|
||||
}
|
||||
|
||||
if (m_fade_intensity[x] == 0) m_fade_pos[x] = m_rows;
|
||||
|
||||
// REMEMBER: y is a number from 0 to m_rows, 0 means all blocks are glowing,
|
||||
// m_rows means none are
|
||||
canvas_painter.drawPixmap(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, *bar(),
|
||||
0, y * (HEIGHT + 1), bar()->width(),
|
||||
bar()->height());
|
||||
}
|
||||
|
||||
for (uint x = 0; x < m_store.size(); ++x)
|
||||
canvas_painter.drawPixmap(x * (WIDTH + 1), int(m_store[x]) * (HEIGHT + 1) + m_y, m_topBarPixmap);
|
||||
|
||||
p.drawPixmap(0, 0, canvas_);
|
||||
}
|
||||
|
||||
static inline void adjustToLimits(int& b, int& f, uint& amount) {
|
||||
// with a range of 0-255 and maximum adjustment of amount,
|
||||
// maximise the difference between f and b
|
||||
|
||||
if (b < f) {
|
||||
if (b > 255 - f) {
|
||||
amount -= f;
|
||||
f = 0;
|
||||
}
|
||||
else {
|
||||
amount -= (255 - f);
|
||||
f = 255;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (f > 255 - b) {
|
||||
amount -= f;
|
||||
f = 0;
|
||||
}
|
||||
else {
|
||||
amount -= (255 - f);
|
||||
f = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clever contrast function
|
||||
*
|
||||
* It will try to adjust the foreground color such that it contrasts well with
|
||||
*the background
|
||||
* It won't modify the hue of fg unless absolutely necessary
|
||||
* @return the adjusted form of fg
|
||||
*/
|
||||
QColor ensureContrast(const QColor& bg, const QColor& fg, uint _amount = 150) {
|
||||
class OutputOnExit {
|
||||
public:
|
||||
OutputOnExit(const QColor& color) : c(color) {}
|
||||
~OutputOnExit() {
|
||||
int h, s, v;
|
||||
c.getHsv(&h, &s, &v);
|
||||
}
|
||||
|
||||
private:
|
||||
const QColor& c;
|
||||
};
|
||||
|
||||
// hack so I don't have to cast everywhere
|
||||
#define amount static_cast<int>(_amount)
|
||||
// #define STAMP debug() << (QValueList<int>() << fh << fs << fv) << endl;
|
||||
// #define STAMP1( string ) debug() << string << ": " <<
|
||||
// (QValueList<int>() << fh << fs << fv) << endl;
|
||||
// #define STAMP2( string, value ) debug() << string << "=" << value << ":
|
||||
// " << (QValueList<int>() << fh << fs << fv) << endl;
|
||||
|
||||
OutputOnExit allocateOnTheStack(fg);
|
||||
|
||||
int bh, bs, bv;
|
||||
int fh, fs, fv;
|
||||
|
||||
bg.getHsv(&bh, &bs, &bv);
|
||||
fg.getHsv(&fh, &fs, &fv);
|
||||
|
||||
int dv = abs(bv - fv);
|
||||
|
||||
// STAMP2( "DV", dv );
|
||||
|
||||
// value is the best measure of contrast
|
||||
// if there is enough difference in value already, return fg unchanged
|
||||
if (dv > amount) return fg;
|
||||
|
||||
int ds = abs(bs - fs);
|
||||
|
||||
// STAMP2( "DS", ds );
|
||||
|
||||
// saturation is good enough too. But not as good. TODO adapt this a little
|
||||
if (ds > amount) return fg;
|
||||
|
||||
int dh = abs(bh - fh);
|
||||
|
||||
// STAMP2( "DH", dh );
|
||||
|
||||
if (dh > 120) {
|
||||
// a third of the colour wheel automatically guarentees contrast
|
||||
// but only if the values are high enough and saturations significant enough
|
||||
// to allow the colours to be visible and not be shades of grey or black
|
||||
|
||||
// check the saturation for the two colours is sufficient that hue alone can
|
||||
// provide sufficient contrast
|
||||
if (ds > amount / 2 && (bs > 125 && fs > 125))
|
||||
// STAMP1( "Sufficient saturation difference, and hues are
|
||||
// compliemtary" );
|
||||
return fg;
|
||||
else if (dv > amount / 2 && (bv > 125 && fv > 125))
|
||||
// STAMP1( "Sufficient value difference, and hues are
|
||||
// compliemtary" );
|
||||
return fg;
|
||||
|
||||
// STAMP1( "Hues are complimentary but we must modify the value or
|
||||
// saturation of the contrasting colour" );
|
||||
|
||||
// but either the colours are two desaturated, or too dark
|
||||
// so we need to adjust the system, although not as much
|
||||
///_amount /= 2;
|
||||
}
|
||||
|
||||
if (fs < 50 && ds < 40) {
|
||||
// low saturation on a low saturation is sad
|
||||
const int tmp = 50 - fs;
|
||||
fs = 50;
|
||||
if (amount > tmp)
|
||||
_amount -= tmp;
|
||||
else
|
||||
_amount = 0;
|
||||
}
|
||||
|
||||
// test that there is available value to honor our contrast requirement
|
||||
if (255 - dv < amount) {
|
||||
// we have to modify the value and saturation of fg
|
||||
// adjustToLimits( bv, fv, amount );
|
||||
|
||||
// STAMP
|
||||
|
||||
// see if we need to adjust the saturation
|
||||
if (amount > 0) adjustToLimits(bs, fs, _amount);
|
||||
|
||||
// STAMP
|
||||
|
||||
// see if we need to adjust the hue
|
||||
if (amount > 0) fh += amount; // cycles around;
|
||||
|
||||
// STAMP
|
||||
|
||||
return QColor::fromHsv(fh, fs, fv);
|
||||
}
|
||||
|
||||
// STAMP
|
||||
|
||||
if (fv > bv && bv > amount) return QColor::fromHsv(fh, fs, bv - amount);
|
||||
|
||||
// STAMP
|
||||
|
||||
if (fv < bv && fv > amount) return QColor::fromHsv(fh, fs, fv - amount);
|
||||
|
||||
// STAMP
|
||||
|
||||
if (fv > bv && (255 - fv > amount))
|
||||
return QColor::fromHsv(fh, fs, fv + amount);
|
||||
|
||||
// STAMP
|
||||
|
||||
if (fv < bv && (255 - bv > amount))
|
||||
return QColor::fromHsv(fh, fs, bv + amount);
|
||||
|
||||
// STAMP
|
||||
// debug() << "Something went wrong!\n";
|
||||
|
||||
return Qt::blue;
|
||||
|
||||
#undef amount
|
||||
// #undef STAMP
|
||||
}
|
||||
|
||||
void BlockAnalyzer::paletteChange(const QPalette&) // virtual
|
||||
{
|
||||
const QColor bg = palette().color(QPalette::Background);
|
||||
const QColor fg = ensureContrast(bg, palette().color(QPalette::Highlight));
|
||||
|
||||
m_topBarPixmap.fill(fg);
|
||||
|
||||
const double dr = 15 * double(bg.red() - fg.red()) / (m_rows * 16);
|
||||
const double dg = 15 * double(bg.green() - fg.green()) / (m_rows * 16);
|
||||
const double db = 15 * double(bg.blue() - fg.blue()) / (m_rows * 16);
|
||||
const int r = fg.red(), g = fg.green(), b = fg.blue();
|
||||
|
||||
bar()->fill(bg);
|
||||
|
||||
QPainter p(bar());
|
||||
for (int y = 0; (uint)y < m_rows; ++y)
|
||||
// graduate the fg color
|
||||
p.fillRect(0, y * (HEIGHT + 1), WIDTH, HEIGHT, QColor(r + int(dr * y), g + int(dg * y), b + int(db * y)));
|
||||
|
||||
{
|
||||
const QColor bg = palette().color(QPalette::Background).dark(112);
|
||||
|
||||
// make a complimentary fadebar colour
|
||||
// TODO dark is not always correct, dumbo!
|
||||
int h, s, v;
|
||||
palette().color(QPalette::Background).dark(150).getHsv(&h, &s, &v);
|
||||
const QColor fg(QColor::fromHsv(h + 120, s, v));
|
||||
|
||||
const double dr = fg.red() - bg.red();
|
||||
const double dg = fg.green() - bg.green();
|
||||
const double db = fg.blue() - bg.blue();
|
||||
const int r = bg.red(), g = bg.green(), b = bg.blue();
|
||||
|
||||
// Precalculate all fade-bar pixmaps
|
||||
for (uint y = 0; y < FADE_SIZE; ++y) {
|
||||
m_fade_bars[y].fill(palette().color(QPalette::Background));
|
||||
QPainter f(&m_fade_bars[y]);
|
||||
for (int z = 0; (uint)z < m_rows; ++z) {
|
||||
const double Y = 1.0 - (log10(FADE_SIZE - y) / log10(FADE_SIZE));
|
||||
f.fillRect(0, z * (HEIGHT + 1), WIDTH, HEIGHT, QColor(r + int(dr * Y), g + int(dg * Y), b + int(db * Y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawBackground();
|
||||
}
|
||||
|
||||
void BlockAnalyzer::drawBackground() {
|
||||
const QColor bg = palette().color(QPalette::Background);
|
||||
const QColor bgdark = bg.dark(112);
|
||||
|
||||
m_background.fill(bg);
|
||||
|
||||
QPainter p(&m_background);
|
||||
for (int x = 0; (uint)x < m_columns; ++x)
|
||||
for (int y = 0; (uint)y < m_rows; ++y)
|
||||
p.fillRect(x * (WIDTH + 1), y * (HEIGHT + 1) + m_y, WIDTH, HEIGHT, bgdark);
|
||||
}
|
||||
65
src/analyzer/blockanalyzer.h
Normal file
65
src/analyzer/blockanalyzer.h
Normal file
@@ -0,0 +1,65 @@
|
||||
// Maintainer: Max Howell <mac.howell@methylblue.com>, (C) 2003-5
|
||||
// Copyright: See COPYING file that comes with this distribution
|
||||
//
|
||||
|
||||
#ifndef BLOCKANALYZER_H
|
||||
#define BLOCKANALYZER_H
|
||||
|
||||
#include "analyzerbase.h"
|
||||
#include <qcolor.h>
|
||||
|
||||
class QResizeEvent;
|
||||
class QMouseEvent;
|
||||
class QPalette;
|
||||
|
||||
/**
|
||||
* @author Max Howell
|
||||
*/
|
||||
|
||||
class BlockAnalyzer : public Analyzer::Base {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE BlockAnalyzer(QWidget*);
|
||||
~BlockAnalyzer();
|
||||
|
||||
static const uint HEIGHT;
|
||||
static const uint WIDTH;
|
||||
static const uint MIN_ROWS;
|
||||
static const uint MIN_COLUMNS;
|
||||
static const uint MAX_COLUMNS;
|
||||
static const uint FADE_SIZE;
|
||||
|
||||
static const char *kName;
|
||||
|
||||
protected:
|
||||
virtual void transform(Analyzer::Scope&);
|
||||
virtual void analyze(QPainter &p, const Analyzer::Scope&, bool new_frame);
|
||||
virtual void resizeEvent(QResizeEvent*);
|
||||
virtual void paletteChange(const QPalette&);
|
||||
virtual void framerateChanged();
|
||||
|
||||
void drawBackground();
|
||||
void determineStep();
|
||||
|
||||
private:
|
||||
QPixmap* bar() { return &m_barPixmap; }
|
||||
|
||||
uint m_columns, m_rows; // number of rows and columns of blocks
|
||||
uint m_y; // y-offset from top of widget
|
||||
QPixmap m_barPixmap;
|
||||
QPixmap m_topBarPixmap;
|
||||
QPixmap m_background;
|
||||
QPixmap canvas_;
|
||||
Analyzer::Scope m_scope; // so we don't create a vector every frame
|
||||
std::vector<float> m_store; // current bar heights
|
||||
std::vector<float> m_yscale;
|
||||
|
||||
// FIXME why can't I namespace these? c++ issue?
|
||||
std::vector<QPixmap> m_fade_bars;
|
||||
std::vector<uint> m_fade_pos;
|
||||
std::vector<int> m_fade_intensity;
|
||||
|
||||
float m_step; // rows to fall per frame
|
||||
};
|
||||
|
||||
#endif
|
||||
203
src/analyzer/fht.cpp
Normal file
203
src/analyzer/fht.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
// FHT - Fast Hartley Transform Class
|
||||
//
|
||||
// Copyright (C) 2004 Melchior FRANZ - mfranz@kde.org
|
||||
//
|
||||
// This program 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 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program 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 this program; if not, write to the Free Software
|
||||
// Foundation, 51 Franklin Steet, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
//
|
||||
// $Id$
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include "fht.h"
|
||||
|
||||
FHT::FHT(int n) : m_buf(0), m_tab(0), m_log(0) {
|
||||
if (n < 3) {
|
||||
m_num = 0;
|
||||
m_exp2 = -1;
|
||||
return;
|
||||
}
|
||||
m_exp2 = n;
|
||||
m_num = 1 << n;
|
||||
if (n > 3) {
|
||||
m_buf = new float[m_num];
|
||||
m_tab = new float[m_num * 2];
|
||||
makeCasTable();
|
||||
}
|
||||
}
|
||||
|
||||
FHT::~FHT() {
|
||||
delete[] m_buf;
|
||||
delete[] m_tab;
|
||||
delete[] m_log;
|
||||
}
|
||||
|
||||
void FHT::makeCasTable(void) {
|
||||
float d, *costab, *sintab;
|
||||
int ul, ndiv2 = m_num / 2;
|
||||
|
||||
for (costab = m_tab, sintab = m_tab + m_num / 2 + 1, ul = 0; ul < m_num; ul++) {
|
||||
d = M_PI * ul / ndiv2;
|
||||
*costab = *sintab = cos(d);
|
||||
|
||||
costab += 2, sintab += 2;
|
||||
if (sintab > m_tab + m_num * 2) sintab = m_tab + 1;
|
||||
}
|
||||
}
|
||||
|
||||
float* FHT::copy(float* d, float* s) {
|
||||
return (float*)memcpy(d, s, m_num * sizeof(float));
|
||||
}
|
||||
|
||||
float* FHT::clear(float* d) {
|
||||
return (float*)memset(d, 0, m_num * sizeof(float));
|
||||
}
|
||||
|
||||
void FHT::scale(float* p, float d) {
|
||||
for (int i = 0; i < (m_num / 2); i++) *p++ *= d;
|
||||
}
|
||||
|
||||
void FHT::ewma(float* d, float* s, float w) {
|
||||
for (int i = 0; i < (m_num / 2); i++, d++, s++) *d = *d * w + *s * (1 - w);
|
||||
}
|
||||
|
||||
void FHT::logSpectrum(float* out, float* p) {
|
||||
int n = m_num / 2, i, j, k, *r;
|
||||
if (!m_log) {
|
||||
m_log = new int[n];
|
||||
float f = n / log10((double)n);
|
||||
for (i = 0, r = m_log; i < n; i++, r++) {
|
||||
j = int(rint(log10(i + 1.0) * f));
|
||||
*r = j >= n ? n - 1 : j;
|
||||
}
|
||||
}
|
||||
semiLogSpectrum(p);
|
||||
*out++ = *p = *p / 100;
|
||||
for (k = i = 1, r = m_log; i < n; i++) {
|
||||
j = *r++;
|
||||
if (i == j)
|
||||
*out++ = p[i];
|
||||
else {
|
||||
float base = p[k - 1];
|
||||
float step = (p[j] - base) / (j - (k - 1));
|
||||
for (float corr = 0; k <= j; k++, corr += step) *out++ = base + corr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FHT::semiLogSpectrum(float* p) {
|
||||
float e;
|
||||
power2(p);
|
||||
for (int i = 0; i < (m_num / 2); i++, p++) {
|
||||
e = 10.0 * log10(sqrt(*p * .5));
|
||||
*p = e < 0 ? 0 : e;
|
||||
}
|
||||
}
|
||||
|
||||
void FHT::spectrum(float* p) {
|
||||
power2(p);
|
||||
for (int i = 0; i < (m_num / 2); i++, p++) *p = (float)sqrt(*p * .5);
|
||||
}
|
||||
|
||||
void FHT::power(float* p) {
|
||||
power2(p);
|
||||
for (int i = 0; i < (m_num / 2); i++) *p++ *= .5;
|
||||
}
|
||||
|
||||
void FHT::power2(float* p) {
|
||||
int i;
|
||||
float* q;
|
||||
_transform(p, m_num, 0);
|
||||
|
||||
*p = (*p * *p), *p += *p, p++;
|
||||
|
||||
for (i = 1, q = p + m_num - 2; i < (m_num / 2); i++, --q)
|
||||
*p = (*p * *p) + (*q * *q), p++;
|
||||
}
|
||||
|
||||
void FHT::transform(float* p) {
|
||||
if (m_num == 8)
|
||||
transform8(p);
|
||||
else
|
||||
_transform(p, m_num, 0);
|
||||
}
|
||||
|
||||
void FHT::transform8(float* p) {
|
||||
float a, b, c, d, e, f, g, h, b_f2, d_h2;
|
||||
float a_c_eg, a_ce_g, ac_e_g, aceg, b_df_h, bdfh;
|
||||
|
||||
a = *p++, b = *p++, c = *p++, d = *p++;
|
||||
e = *p++, f = *p++, g = *p++, h = *p;
|
||||
b_f2 = (b - f) * M_SQRT2;
|
||||
d_h2 = (d - h) * M_SQRT2;
|
||||
|
||||
a_c_eg = a - c - e + g;
|
||||
a_ce_g = a - c + e - g;
|
||||
ac_e_g = a + c - e - g;
|
||||
aceg = a + c + e + g;
|
||||
|
||||
b_df_h = b - d + f - h;
|
||||
bdfh = b + d + f + h;
|
||||
|
||||
*p = a_c_eg - d_h2;
|
||||
*--p = a_ce_g - b_df_h;
|
||||
*--p = ac_e_g - b_f2;
|
||||
*--p = aceg - bdfh;
|
||||
*--p = a_c_eg + d_h2;
|
||||
*--p = a_ce_g + b_df_h;
|
||||
*--p = ac_e_g + b_f2;
|
||||
*--p = aceg + bdfh;
|
||||
}
|
||||
|
||||
void FHT::_transform(float* p, int n, int k) {
|
||||
|
||||
if (n == 8) {
|
||||
transform8(p + k);
|
||||
return;
|
||||
}
|
||||
|
||||
int i, j, ndiv2 = n / 2;
|
||||
float a, *t1, *t2, *t3, *t4, *ptab, *pp;
|
||||
|
||||
for (i = 0, t1 = m_buf, t2 = m_buf + ndiv2, pp = &p[k]; i < ndiv2; i++)
|
||||
*t1++ = *pp++, *t2++ = *pp++;
|
||||
|
||||
memcpy(p + k, m_buf, sizeof(float) * n);
|
||||
|
||||
_transform(p, ndiv2, k);
|
||||
_transform(p, ndiv2, k + ndiv2);
|
||||
|
||||
j = m_num / ndiv2 - 1;
|
||||
t1 = m_buf;
|
||||
t2 = t1 + ndiv2;
|
||||
t3 = p + k + ndiv2;
|
||||
ptab = m_tab;
|
||||
pp = p + k;
|
||||
|
||||
a = *ptab++ * *t3++;
|
||||
a += *ptab * *pp;
|
||||
ptab += j;
|
||||
|
||||
*t1++ = *pp + a;
|
||||
*t2++ = *pp++ - a;
|
||||
|
||||
for (i = 1, t4 = p + k + n; i < ndiv2; i++, ptab += j) {
|
||||
a = *ptab++ * *t3++;
|
||||
a += *ptab * *--t4;
|
||||
|
||||
*t1++ = *pp + a;
|
||||
*t2++ = *pp++ - a;
|
||||
}
|
||||
memcpy(p + k, m_buf, sizeof(float) * n);
|
||||
}
|
||||
118
src/analyzer/fht.h
Normal file
118
src/analyzer/fht.h
Normal file
@@ -0,0 +1,118 @@
|
||||
// FHT - Fast Hartley Transform Class
|
||||
//
|
||||
// Copyright (C) 2004 Melchior FRANZ - mfranz@kde.org
|
||||
//
|
||||
// This program 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 2 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program 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 this program; if not, write to the Free Software
|
||||
// Foundation, 51 Franklin Steet, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
//
|
||||
// $Id$
|
||||
|
||||
#ifndef FHT_H
|
||||
#define FHT_H
|
||||
|
||||
/**
|
||||
* Implementation of the Hartley Transform after Bracewell's discrete
|
||||
* algorithm. The algorithm is subject to US patent No. 4,646,256 (1987)
|
||||
* but was put into public domain by the Board of Trustees of Stanford
|
||||
* University in 1994 and is now freely available[1].
|
||||
*
|
||||
* [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379
|
||||
*/
|
||||
class FHT {
|
||||
int m_exp2;
|
||||
int m_num;
|
||||
float* m_buf;
|
||||
float* m_tab;
|
||||
int* m_log;
|
||||
|
||||
/**
|
||||
* Create a table of "cas" (cosine and sine) values.
|
||||
* Has only to be done in the constructor and saves from
|
||||
* calculating the same values over and over while transforming.
|
||||
*/
|
||||
void makeCasTable();
|
||||
|
||||
/**
|
||||
* Recursive in-place Hartley transform. For internal use only!
|
||||
*/
|
||||
void _transform(float*, int, int);
|
||||
|
||||
public:
|
||||
/**
|
||||
* Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$
|
||||
* should be at least 3. Values of more than 3 need a trigonometry table.
|
||||
* @see makeCasTable()
|
||||
*/
|
||||
FHT(int);
|
||||
|
||||
~FHT();
|
||||
inline int sizeExp() const { return m_exp2; }
|
||||
inline int size() const { return m_num; }
|
||||
float* copy(float*, float*);
|
||||
float* clear(float*);
|
||||
void scale(float*, float);
|
||||
|
||||
/**
|
||||
* Exponentially Weighted Moving Average (EWMA) filter.
|
||||
* @param d is the filtered data.
|
||||
* @param s is fresh input.
|
||||
* @param w is the weighting factor.
|
||||
*/
|
||||
void ewma(float* d, float* s, float w);
|
||||
|
||||
/**
|
||||
* Logarithmic audio spectrum. Maps semi-logarithmic spectrum
|
||||
* to logarithmic frequency scale, interpolates missing values.
|
||||
* A logarithmic index map is calculated at the first run only.
|
||||
* @param p is the input array.
|
||||
* @param out is the spectrum.
|
||||
*/
|
||||
void logSpectrum(float* out, float* p);
|
||||
|
||||
/**
|
||||
* Semi-logarithmic audio spectrum.
|
||||
*/
|
||||
void semiLogSpectrum(float*);
|
||||
|
||||
/**
|
||||
* Fourier spectrum.
|
||||
*/
|
||||
void spectrum(float*);
|
||||
|
||||
/**
|
||||
* Calculates a mathematically correct FFT power spectrum.
|
||||
* If further scaling is applied later, use power2 instead
|
||||
* and factor the 0.5 in the final scaling factor.
|
||||
* @see FHT::power2()
|
||||
*/
|
||||
void power(float*);
|
||||
|
||||
/**
|
||||
* Calculates an FFT power spectrum with doubled values as a
|
||||
* result. The values need to be multiplied by 0.5 to be exact.
|
||||
* Note that you only get @f$2^{n-1}@f$ power values for a data set
|
||||
* of @f$2^n@f$ input values. This is the fastest transform.
|
||||
* @see FHT::power()
|
||||
*/
|
||||
void power2(float*);
|
||||
|
||||
/**
|
||||
* Discrete Hartley transform of data sets with 8 values.
|
||||
*/
|
||||
void transform8(float*);
|
||||
|
||||
void transform(float*);
|
||||
};
|
||||
|
||||
#endif
|
||||
12
src/cmakelists-check.sh
Executable file
12
src/cmakelists-check.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
for f in `find .`
|
||||
do
|
||||
file=$(basename $f)
|
||||
grep -i $file CMakeLists.txt >/dev/null 2>&1
|
||||
#echo $?
|
||||
if [ $? -eq 0 ]; then
|
||||
continue
|
||||
fi
|
||||
echo "$file not in CMakeLists.txt"
|
||||
done
|
||||
155
src/collection/collection.cpp
Normal file
155
src/collection/collection.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <QThread>
|
||||
|
||||
#include "collection.h"
|
||||
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/thread.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
const char *Collection::kSongsTable = "songs";
|
||||
const char *Collection::kDirsTable = "directories";
|
||||
const char *Collection::kSubdirsTable = "subdirectories";
|
||||
const char *Collection::kFtsTable = "songs_fts";
|
||||
|
||||
Collection::Collection(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
backend_(nullptr),
|
||||
model_(nullptr),
|
||||
watcher_(nullptr),
|
||||
watcher_thread_(nullptr) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
backend_ = new CollectionBackend;
|
||||
backend()->moveToThread(app->database()->thread());
|
||||
|
||||
backend_->Init(app->database(), kSongsTable, kDirsTable, kSubdirsTable, kFtsTable);
|
||||
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
Collection::~Collection() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_->deleteLater();
|
||||
watcher_thread_->exit();
|
||||
watcher_thread_->wait(5000 /* five seconds */);
|
||||
}
|
||||
|
||||
void Collection::Init() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_ = new CollectionWatcher;
|
||||
watcher_thread_ = new Thread(this);
|
||||
watcher_thread_->SetIoPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
watcher_->moveToThread(watcher_thread_);
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
|
||||
watcher_->set_backend(backend_);
|
||||
watcher_->set_task_manager(app_->task_manager());
|
||||
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), watcher_, SLOT(AddDirectory(Directory, SubdirectoryList)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), watcher_, SLOT(RemoveDirectory(Directory)));
|
||||
connect(watcher_, SIGNAL(NewOrUpdatedSongs(SongList)), backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsMTimeUpdated(SongList)), backend_, SLOT(UpdateMTimesOnly(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsDeleted(SongList)), backend_, SLOT(MarkSongsUnavailable(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsReadded(SongList, bool)), backend_, SLOT(MarkSongsUnavailable(SongList, bool)));
|
||||
connect(watcher_, SIGNAL(SubdirsDiscovered(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(CompilationsNeedUpdating()), backend_, SLOT(UpdateCompilations()));
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
|
||||
connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped()));
|
||||
|
||||
// This will start the watcher checking for updates
|
||||
backend_->LoadDirectoriesAsync();
|
||||
}
|
||||
|
||||
void Collection::IncrementalScan() { watcher_->IncrementalScanAsync(); }
|
||||
|
||||
void Collection::FullScan() { watcher_->FullScanAsync(); }
|
||||
|
||||
void Collection::PauseWatcher() { watcher_->SetRescanPausedAsync(true); }
|
||||
|
||||
void Collection::ResumeWatcher() { watcher_->SetRescanPausedAsync(false); }
|
||||
|
||||
void Collection::ReloadSettings() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
watcher_->ReloadSettingsAsync();
|
||||
|
||||
}
|
||||
|
||||
void Collection::Stopped() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
CurrentSongChanged(Song());
|
||||
}
|
||||
|
||||
void Collection::CurrentSongChanged(const Song &song) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
TagReaderReply *reply = nullptr;
|
||||
|
||||
if (reply) {
|
||||
connect(reply, SIGNAL(Finished(bool)), reply, SLOT(deleteLater()));
|
||||
}
|
||||
|
||||
if (song.filetype() == Song::Type_Asf) {
|
||||
current_wma_song_url_ = song.url();
|
||||
}
|
||||
}
|
||||
|
||||
SongList Collection::FilterCurrentWMASong(SongList songs, Song* queued) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
for (SongList::iterator it = songs.begin(); it != songs.end(); ) {
|
||||
if (it->url() == current_wma_song_url_) {
|
||||
*queued = *it;
|
||||
it = songs.erase(it);
|
||||
}
|
||||
else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
return songs;
|
||||
}
|
||||
98
src/collection/collection.h
Normal file
98
src/collection/collection.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTION_H
|
||||
#define COLLECTION_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class Application;
|
||||
class Database;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class CollectionWatcher;
|
||||
class TaskManager;
|
||||
class Thread;
|
||||
|
||||
class Collection : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Collection(Application* app, QObject* parent);
|
||||
~Collection();
|
||||
|
||||
static const char *kSongsTable;
|
||||
static const char *kDirsTable;
|
||||
static const char *kSubdirsTable;
|
||||
static const char *kFtsTable;
|
||||
|
||||
void Init();
|
||||
|
||||
CollectionBackend *backend() const { return backend_; }
|
||||
CollectionModel *model() const { return model_; }
|
||||
|
||||
QString full_rescan_reason(int schema_version) const { return full_rescan_revisions_.value(schema_version, QString()); }
|
||||
|
||||
int Total_Albums = 0;
|
||||
int total_songs_ = 0;
|
||||
int Total_Artists = 0;
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void PauseWatcher();
|
||||
void ResumeWatcher();
|
||||
|
||||
void FullScan();
|
||||
|
||||
private slots:
|
||||
void IncrementalScan();
|
||||
|
||||
void CurrentSongChanged(const Song &song);
|
||||
void Stopped();
|
||||
|
||||
private:
|
||||
SongList FilterCurrentWMASong(SongList songs, Song* queued);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
CollectionBackend *backend_;
|
||||
CollectionModel *model_;
|
||||
|
||||
CollectionWatcher *watcher_;
|
||||
Thread *watcher_thread_;
|
||||
|
||||
// Hack: Gstreamer doesn't cope well with WMA files being rewritten while
|
||||
// being played, so we delay statistics and rating changes until the current
|
||||
// song has finished playing.
|
||||
QUrl current_wma_song_url_;
|
||||
|
||||
// DB schema versions which should trigger a full collection rescan (each of
|
||||
// those with a short reason why).
|
||||
QHash<int, QString> full_rescan_revisions_;
|
||||
};
|
||||
|
||||
#endif
|
||||
1132
src/collection/collectionbackend.cpp
Normal file
1132
src/collection/collectionbackend.cpp
Normal file
File diff suppressed because it is too large
Load Diff
232
src/collection/collectionbackend.h
Normal file
232
src/collection/collectionbackend.h
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONBACKEND_H
|
||||
#define COLLECTIONBACKEND_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QUrl>
|
||||
|
||||
#include "directory.h"
|
||||
#include "collectionquery.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class Database;
|
||||
|
||||
class CollectionBackendInterface : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionBackendInterface(QObject *parent = nullptr) : QObject(parent) {}
|
||||
virtual ~CollectionBackendInterface() {}
|
||||
|
||||
struct Album {
|
||||
Album() {}
|
||||
Album(const QString &_artist, const QString &_album_artist, const QString &_album_name, const QString &_art_automatic, const QString &_art_manual, const QUrl &_first_url) :
|
||||
artist(_artist),
|
||||
album_artist(_album_artist),
|
||||
album_name(_album_name),
|
||||
art_automatic(_art_automatic),
|
||||
art_manual(_art_manual),
|
||||
first_url(_first_url) {}
|
||||
|
||||
const QString &effective_albumartist() const {
|
||||
return album_artist.isEmpty() ? artist : album_artist;
|
||||
}
|
||||
|
||||
QString artist;
|
||||
QString album_artist;
|
||||
QString album_name;
|
||||
|
||||
QString art_automatic;
|
||||
QString art_manual;
|
||||
QUrl first_url;
|
||||
};
|
||||
typedef QList<Album> AlbumList;
|
||||
|
||||
virtual QString songs_table() const = 0;
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
virtual void LoadDirectoriesAsync() = 0;
|
||||
|
||||
virtual void UpdateTotalSongCountAsync() = 0;
|
||||
virtual void UpdateTotalArtistCountAsync() = 0;
|
||||
virtual void UpdateTotalAlbumCountAsync() = 0;
|
||||
|
||||
virtual SongList FindSongsInDirectory(int id) = 0;
|
||||
virtual SubdirectoryList SubdirsInDirectory(int id) = 0;
|
||||
virtual DirectoryList GetAllDirectories() = 0;
|
||||
virtual void ChangeDirPath(int id, const QString &old_path, const QString &new_path) = 0;
|
||||
|
||||
virtual QStringList GetAllArtists(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions()) = 0;
|
||||
|
||||
virtual void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art) = 0;
|
||||
virtual Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album) = 0;
|
||||
|
||||
virtual Song GetSongById(int id) = 0;
|
||||
|
||||
// Returns all sections of a song with the given filename. If there's just one section
|
||||
// the resulting list will have it's size equal to 1.
|
||||
virtual SongList GetSongsByUrl(const QUrl &url) = 0;
|
||||
// Returns a section of a song with the given filename and beginning. If the section
|
||||
// is not present in collection, returns invalid song.
|
||||
// Using default beginning value is suitable when searching for single-section songs.
|
||||
virtual Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) = 0;
|
||||
|
||||
virtual void AddDirectory(const QString &path) = 0;
|
||||
virtual void RemoveDirectory(const Directory &dir) = 0;
|
||||
|
||||
virtual bool ExecQuery(CollectionQuery *q) = 0;
|
||||
};
|
||||
|
||||
class CollectionBackend : public CollectionBackendInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
Q_INVOKABLE CollectionBackend(QObject *parent = nullptr);
|
||||
void Init(Database *db, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table, const QString &fts_table);
|
||||
|
||||
Database *db() const { return db_; }
|
||||
|
||||
QString songs_table() const { return songs_table_; }
|
||||
QString dirs_table() const { return dirs_table_; }
|
||||
QString subdirs_table() const { return subdirs_table_; }
|
||||
|
||||
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
|
||||
void LoadDirectoriesAsync();
|
||||
|
||||
void UpdateTotalSongCountAsync();
|
||||
void UpdateTotalArtistCountAsync();
|
||||
void UpdateTotalAlbumCountAsync();
|
||||
|
||||
SongList FindSongsInDirectory(int id);
|
||||
SubdirectoryList SubdirsInDirectory(int id);
|
||||
DirectoryList GetAllDirectories();
|
||||
void ChangeDirPath(int id, const QString &old_path, const QString &new_path);
|
||||
|
||||
QStringList GetAll(const QString &column, const QueryOptions &opt = QueryOptions());
|
||||
QStringList GetAllArtists(const QueryOptions &opt = QueryOptions());
|
||||
QStringList GetAllArtistsWithAlbums(const QueryOptions &opt = QueryOptions());
|
||||
SongList GetSongsByAlbum(const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
SongList GetSongs(const QString &artist, const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
|
||||
SongList GetCompilationSongs(const QString &album, const QueryOptions &opt = QueryOptions());
|
||||
|
||||
AlbumList GetAllAlbums(const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetAlbumsByArtist(const QString &artist, const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetAlbumsByAlbumArtist(const QString &albumartist, const QueryOptions &opt = QueryOptions());
|
||||
AlbumList GetCompilationAlbums(const QueryOptions &opt = QueryOptions());
|
||||
|
||||
void UpdateManualAlbumArtAsync(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
|
||||
Album GetAlbumArt(const QString &artist, const QString &albumartist, const QString &album);
|
||||
|
||||
Song GetSongById(int id);
|
||||
SongList GetSongsById(const QList<int> &ids);
|
||||
SongList GetSongsById(const QStringList &ids);
|
||||
SongList GetSongsByForeignId(const QStringList &ids, const QString &table, const QString &column);
|
||||
|
||||
SongList GetSongsByUrl(const QUrl &url);
|
||||
Song GetSongByUrl(const QUrl &url, qint64 beginning = 0);
|
||||
|
||||
void AddDirectory(const QString &path);
|
||||
void RemoveDirectory(const Directory &dir);
|
||||
|
||||
bool ExecQuery(CollectionQuery *q);
|
||||
SongList ExecCollectionQuery(CollectionQuery *query);
|
||||
|
||||
void IncrementPlayCountAsync(int id);
|
||||
void IncrementSkipCountAsync(int id, float progress);
|
||||
void ResetStatisticsAsync(int id);
|
||||
|
||||
void DeleteAll();
|
||||
|
||||
public slots:
|
||||
void LoadDirectories();
|
||||
void UpdateTotalSongCount();
|
||||
void UpdateTotalArtistCount();
|
||||
void UpdateTotalAlbumCount();
|
||||
void AddOrUpdateSongs(const SongList &songs);
|
||||
void UpdateMTimesOnly(const SongList &songs);
|
||||
void DeleteSongs(const SongList &songs);
|
||||
void MarkSongsUnavailable(const SongList &songs, bool unavailable = true);
|
||||
void AddOrUpdateSubdirs(const SubdirectoryList &subdirs);
|
||||
void UpdateCompilations();
|
||||
void UpdateManualAlbumArt(const QString &artist, const QString &albumartist, const QString &album, const QString &art);
|
||||
void ForceCompilation(const QString &album, const QList<QString> &artists, bool on);
|
||||
void IncrementPlayCount(int id);
|
||||
void IncrementSkipCount(int id, float progress);
|
||||
void ResetStatistics(int id);
|
||||
|
||||
signals:
|
||||
void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
|
||||
void DirectoryDeleted(const Directory &dir);
|
||||
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
|
||||
void DatabaseReset();
|
||||
|
||||
void TotalSongCountUpdated(int total);
|
||||
void TotalArtistCountUpdated(int total);
|
||||
void TotalAlbumCountUpdated(int total);
|
||||
|
||||
private:
|
||||
struct CompilationInfo {
|
||||
CompilationInfo() : has_compilation_detected(false), has_not_compilation_detected(false) {}
|
||||
|
||||
QSet<QString> artists;
|
||||
QSet<QString> directories;
|
||||
|
||||
bool has_compilation_detected;
|
||||
bool has_not_compilation_detected;
|
||||
};
|
||||
|
||||
void UpdateCompilations(QSqlQuery &find_songs, QSqlQuery &update, SongList &deleted_songs, SongList &added_songs, const QString &album, int compilation_detected);
|
||||
AlbumList GetAlbums(const QString &artist, const QString &album_artist, bool compilation = false, const QueryOptions &opt = QueryOptions());
|
||||
SubdirectoryList SubdirsInDirectory(int id, QSqlDatabase &db);
|
||||
|
||||
Song GetSongById(int id, QSqlDatabase &db);
|
||||
SongList GetSongsById(const QStringList &ids, QSqlDatabase &db);
|
||||
|
||||
private:
|
||||
Database *db_;
|
||||
QString songs_table_;
|
||||
QString dirs_table_;
|
||||
QString subdirs_table_;
|
||||
QString fts_table_;
|
||||
|
||||
};
|
||||
|
||||
#endif // COLLECTIONBACKEND_H
|
||||
|
||||
110
src/collection/collectiondirectorymodel.cpp
Normal file
110
src/collection/collectiondirectorymodel.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectiondirectorymodel.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/filesystemmusicstorage.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
CollectionDirectoryModel::CollectionDirectoryModel(CollectionBackend* backend, QObject* parent)
|
||||
: QStandardItemModel(parent),
|
||||
dir_icon_(IconLoader::Load("document-open-folder")),
|
||||
backend_(backend)
|
||||
{
|
||||
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), SLOT(DirectoryDiscovered(Directory)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), SLOT(DirectoryDeleted(Directory)));
|
||||
|
||||
}
|
||||
|
||||
CollectionDirectoryModel::~CollectionDirectoryModel() {}
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDiscovered(const Directory &dir) {
|
||||
|
||||
QStandardItem* item;
|
||||
if (Application::kIsPortable && Utilities::UrlOnSameDriveAsStrawberry(QUrl::fromLocalFile(dir.path))) {
|
||||
item = new QStandardItem(Utilities::GetRelativePathToStrawberryBin(QUrl::fromLocalFile(dir.path)).toLocalFile());
|
||||
}
|
||||
else {
|
||||
item = new QStandardItem(dir.path);
|
||||
}
|
||||
item->setData(dir.id, kIdRole);
|
||||
item->setIcon(dir_icon_);
|
||||
storage_ << std::shared_ptr<MusicStorage>(new FilesystemMusicStorage(dir.path));
|
||||
appendRow(item);
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::DirectoryDeleted(const Directory &dir) {
|
||||
|
||||
for (int i = 0; i < rowCount(); ++i) {
|
||||
if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
|
||||
removeRow(i);
|
||||
storage_.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::AddDirectory(const QString &path) {
|
||||
|
||||
if (!backend_) return;
|
||||
|
||||
backend_->AddDirectory(path);
|
||||
|
||||
}
|
||||
|
||||
void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &index) {
|
||||
|
||||
if (!backend_ || !index.isValid()) return;
|
||||
|
||||
Directory dir;
|
||||
dir.path = index.data().toString();
|
||||
dir.id = index.data(kIdRole).toInt();
|
||||
|
||||
backend_->RemoveDirectory(dir);
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionDirectoryModel::data(const QModelIndex &index, int role) const {
|
||||
|
||||
switch (role) {
|
||||
case MusicStorage::Role_Storage:
|
||||
case MusicStorage::Role_StorageForceConnect:
|
||||
return QVariant::fromValue(storage_[index.row()]);
|
||||
|
||||
case MusicStorage::Role_FreeSpace:
|
||||
return Utilities::FileSystemFreeSpace(data(index, Qt::DisplayRole).toString());
|
||||
|
||||
case MusicStorage::Role_Capacity:
|
||||
return Utilities::FileSystemCapacity(data(index, Qt::DisplayRole).toString());
|
||||
|
||||
default:
|
||||
return QStandardItemModel::data(index, role);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
62
src/collection/collectiondirectorymodel.h
Normal file
62
src/collection/collectiondirectorymodel.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONDIRECTORYMODEL_H
|
||||
#define COLLECTIONDIRECTORYMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include "directory.h"
|
||||
|
||||
class CollectionBackend;
|
||||
class MusicStorage;
|
||||
|
||||
class CollectionDirectoryModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionDirectoryModel(CollectionBackend* backend, QObject *parent = nullptr);
|
||||
~CollectionDirectoryModel();
|
||||
|
||||
// To be called by GUIs
|
||||
void AddDirectory(const QString &path);
|
||||
void RemoveDirectory(const QModelIndex &index);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const;
|
||||
|
||||
private slots:
|
||||
// To be called by the backend
|
||||
void DirectoryDiscovered(const Directory &directories);
|
||||
void DirectoryDeleted(const Directory &directories);
|
||||
|
||||
private:
|
||||
static const int kIdRole = Qt::UserRole + 1;
|
||||
|
||||
QIcon dir_icon_;
|
||||
CollectionBackend* backend_;
|
||||
QList<std::shared_ptr<MusicStorage> > storage_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONDIRECTORYMODEL_H
|
||||
364
src/collection/collectionfilterwidget.cpp
Normal file
364
src/collection/collectionfilterwidget.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <QActionGroup>
|
||||
#include <QInputDialog>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QRegExp>
|
||||
#include <QSettings>
|
||||
#include <QSignalMapper>
|
||||
#include <QTimer>
|
||||
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionquery.h"
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_collectionfilterwidget.h"
|
||||
#include "core/song.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_CollectionFilterWidget),
|
||||
model_(nullptr),
|
||||
group_by_dialog_(new GroupByDialog),
|
||||
filter_delay_(new QTimer(this)),
|
||||
filter_applies_to_model_(true),
|
||||
delay_behaviour_(DelayedOnLargeLibraries) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
// Add the available fields to the tooltip here instead of the ui
|
||||
// file to prevent that they get translated by mistake.
|
||||
QString available_fields = Song::kFtsColumns.join(", ").replace(QRegExp("\\bfts"), "");
|
||||
ui_->filter->setToolTip(ui_->filter->toolTip().arg(available_fields));
|
||||
|
||||
connect(ui_->filter, SIGNAL(returnPressed()), SIGNAL(ReturnPressed()));
|
||||
connect(filter_delay_, SIGNAL(timeout()), SLOT(FilterDelayTimeout()));
|
||||
|
||||
filter_delay_->setInterval(kFilterDelay);
|
||||
filter_delay_->setSingleShot(true);
|
||||
|
||||
// Icons
|
||||
ui_->options->setIcon(IconLoader::Load("configure"));
|
||||
|
||||
// Filter by age
|
||||
QActionGroup *filter_age_group = new QActionGroup(this);
|
||||
filter_age_group->addAction(ui_->filter_age_all);
|
||||
filter_age_group->addAction(ui_->filter_age_today);
|
||||
filter_age_group->addAction(ui_->filter_age_week);
|
||||
filter_age_group->addAction(ui_->filter_age_month);
|
||||
filter_age_group->addAction(ui_->filter_age_three_months);
|
||||
filter_age_group->addAction(ui_->filter_age_year);
|
||||
|
||||
filter_age_menu_ = new QMenu(tr("Show"), this);
|
||||
filter_age_menu_->addActions(filter_age_group->actions());
|
||||
|
||||
filter_age_mapper_ = new QSignalMapper(this);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_all, -1);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_today, 60 * 60 * 24);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_week, 60 * 60 * 24 * 7);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_month, 60 * 60 * 24 * 30);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_three_months, 60 * 60 * 24 * 30 * 3);
|
||||
filter_age_mapper_->setMapping(ui_->filter_age_year, 60 * 60 * 24 * 365);
|
||||
|
||||
connect(ui_->filter_age_all, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_today, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_week, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_month, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_three_months, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
connect(ui_->filter_age_year, SIGNAL(triggered()), filter_age_mapper_, SLOT(map()));
|
||||
|
||||
// "Group by ..."
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
|
||||
group_by_menu_ = new QMenu(tr("Group by"), this);
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
|
||||
connect(ui_->save_grouping, SIGNAL(triggered()), this, SLOT(SaveGroupBy()));
|
||||
connect(ui_->manage_groupings, SIGNAL(triggered()), this, SLOT(ShowGroupingManager()));
|
||||
|
||||
// Collection config menu
|
||||
collection_menu_ = new QMenu(tr("Display options"), this);
|
||||
collection_menu_->setIcon(ui_->options->icon());
|
||||
collection_menu_->addMenu(filter_age_menu_);
|
||||
collection_menu_->addMenu(group_by_menu_);
|
||||
collection_menu_->addAction(ui_->save_grouping);
|
||||
collection_menu_->addAction(ui_->manage_groupings);
|
||||
collection_menu_->addSeparator();
|
||||
ui_->options->setMenu(collection_menu_);
|
||||
|
||||
connect(ui_->filter, SIGNAL(textChanged(QString)), SLOT(FilterTextChanged(QString)));
|
||||
|
||||
}
|
||||
|
||||
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
|
||||
|
||||
void CollectionFilterWidget::UpdateGroupByActions() {
|
||||
|
||||
if (group_by_group_) {
|
||||
disconnect(group_by_group_, 0, 0, 0);
|
||||
delete group_by_group_;
|
||||
}
|
||||
|
||||
group_by_group_ = CreateGroupByActions(this);
|
||||
group_by_menu_->clear();
|
||||
group_by_menu_->addActions(group_by_group_->actions());
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)),
|
||||
SLOT(GroupByClicked(QAction*)));
|
||||
if (model_) {
|
||||
CheckCurrentGrouping(model_->GetGroupBy());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
QActionGroup *CollectionFilterWidget::CreateGroupByActions(QObject *parent) {
|
||||
|
||||
QActionGroup *ret = new QActionGroup(parent);
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_AlbumArtist, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Artist/Year - Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_YearAlbum)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Album)));
|
||||
ret->addAction(CreateGroupByAction(tr("Group by Genre/Artist/Album"), parent, CollectionModel::Grouping(CollectionModel::GroupBy_Genre, CollectionModel::GroupBy_Artist, CollectionModel::GroupBy_Album)));
|
||||
|
||||
QAction *sep1 = new QAction(parent);
|
||||
sep1->setSeparator(true);
|
||||
ret->addAction(sep1);
|
||||
|
||||
// read saved groupings
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
ret->addAction(CreateGroupByAction(saved.at(i), parent, g));
|
||||
}
|
||||
|
||||
QAction *sep2 = new QAction(parent);
|
||||
sep2->setSeparator(true);
|
||||
ret->addAction(sep2);
|
||||
|
||||
ret->addAction(CreateGroupByAction(tr("Advanced grouping..."), parent, CollectionModel::Grouping()));
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QAction *CollectionFilterWidget::CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping &grouping) {
|
||||
|
||||
QAction *ret = new QAction(text, parent);
|
||||
ret->setCheckable(true);
|
||||
|
||||
if (grouping.first != CollectionModel::GroupBy_None) {
|
||||
ret->setProperty("group_by", QVariant::fromValue(grouping));
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SaveGroupBy() {
|
||||
|
||||
QString text = QInputDialog::getText(this, tr("Grouping Name"), tr("Grouping name:"));
|
||||
if (!text.isEmpty() && model_) {
|
||||
model_->SaveGrouping(text);
|
||||
UpdateGroupByActions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ShowGroupingManager() {
|
||||
|
||||
if (!groupings_manager_) {
|
||||
groupings_manager_.reset(new SavedGroupingManager);
|
||||
}
|
||||
groupings_manager_->SetFilter(this);
|
||||
groupings_manager_->UpdateModel();
|
||||
groupings_manager_->show();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void CollectionFilterWidget::FocusOnFilter(QKeyEvent *event) {
|
||||
|
||||
ui_->filter->setFocus();
|
||||
QApplication::sendEvent(ui_->filter, event);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetCollectionModel(CollectionModel *model) {
|
||||
|
||||
if (model_) {
|
||||
disconnect(model_, 0, this, 0);
|
||||
disconnect(model_, 0, group_by_dialog_.get(), 0);
|
||||
disconnect(group_by_dialog_.get(), 0, model_, 0);
|
||||
disconnect(filter_age_mapper_, 0, model_, 0);
|
||||
}
|
||||
|
||||
model_ = model;
|
||||
|
||||
// Connect signals
|
||||
connect(model_, SIGNAL(GroupingChanged(CollectionModel::Grouping)), group_by_dialog_.get(), SLOT(CollectionGroupingChanged(CollectionModel::Grouping)));
|
||||
connect(model_, SIGNAL(GroupingChanged(CollectionModel::Grouping)), SLOT(GroupingChanged(CollectionModel::Grouping)));
|
||||
connect(group_by_dialog_.get(), SIGNAL(Accepted(CollectionModel::Grouping)), model_, SLOT(SetGroupBy(CollectionModel::Grouping)));
|
||||
connect(filter_age_mapper_, SIGNAL(mapped(int)), model_, SLOT(SetFilterAge(int)));
|
||||
|
||||
// Load settings
|
||||
if (!settings_group_.isEmpty()) {
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
model_->SetGroupBy(CollectionModel::Grouping(
|
||||
CollectionModel::GroupBy(s.value("group_by1", int(CollectionModel::GroupBy_Artist)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by2", int(CollectionModel::GroupBy_Album)).toInt()),
|
||||
CollectionModel::GroupBy(s.value("group_by3", int(CollectionModel::GroupBy_None)).toInt())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::GroupByClicked(QAction *action) {
|
||||
if (action->property("group_by").isNull()) {
|
||||
group_by_dialog_->show();
|
||||
return;
|
||||
}
|
||||
|
||||
CollectionModel::Grouping g = action->property("group_by").value<CollectionModel::Grouping>();
|
||||
model_->SetGroupBy(g);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::GroupingChanged(const CollectionModel::Grouping &g) {
|
||||
|
||||
if (!settings_group_.isEmpty()) {
|
||||
// Save the settings
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
s.setValue("group_by1", int(g[0]));
|
||||
s.setValue("group_by2", int(g[1]));
|
||||
s.setValue("group_by3", int(g[2]));
|
||||
}
|
||||
|
||||
// Now make sure the correct action is checked
|
||||
CheckCurrentGrouping(g);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::CheckCurrentGrouping(const CollectionModel::Grouping &g) {
|
||||
|
||||
for (QAction *action : group_by_group_->actions()) {
|
||||
if (action->property("group_by").isNull()) continue;
|
||||
|
||||
if (g == action->property("group_by").value<CollectionModel::Grouping>()) {
|
||||
action->setChecked(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the advanced action
|
||||
group_by_group_->actions().last()->setChecked(true);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetFilterHint(const QString &hint) {
|
||||
ui_->filter->setPlaceholderText(hint);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetQueryMode(QueryOptions::QueryMode query_mode) {
|
||||
|
||||
ui_->filter->clear();
|
||||
ui_->filter->setEnabled(query_mode == QueryOptions::QueryMode_All);
|
||||
|
||||
model_->SetFilterQueryMode(query_mode);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::ShowInCollection(const QString &search) {
|
||||
ui_->filter->setText(search);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetAgeFilterEnabled(bool enabled) {
|
||||
filter_age_menu_->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::SetGroupByEnabled(bool enabled) {
|
||||
group_by_menu_->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::AddMenuAction(QAction *action) {
|
||||
collection_menu_->addAction(action);
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
|
||||
|
||||
switch (e->key()) {
|
||||
case Qt::Key_Up:
|
||||
emit UpPressed();
|
||||
e->accept();
|
||||
break;
|
||||
|
||||
case Qt::Key_Down:
|
||||
emit DownPressed();
|
||||
e->accept();
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
ui_->filter->clear();
|
||||
e->accept();
|
||||
break;
|
||||
}
|
||||
|
||||
QWidget::keyReleaseEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
||||
|
||||
// Searching with one or two characters can be very expensive on the database
|
||||
// even with FTS, so if there are a large number of songs in the database
|
||||
// introduce a small delay before actually filtering the model, so if the
|
||||
// user is typing the first few characters of something it will be quicker.
|
||||
const bool delay = (delay_behaviour_ == AlwaysDelayed) || (delay_behaviour_ == DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
|
||||
if (delay) {
|
||||
filter_delay_->start();
|
||||
}
|
||||
else {
|
||||
filter_delay_->stop();
|
||||
FilterDelayTimeout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionFilterWidget::FilterDelayTimeout() {
|
||||
|
||||
emit Filter(ui_->filter->text());
|
||||
if (filter_applies_to_model_) {
|
||||
model_->SetFilterText(ui_->filter->text());
|
||||
}
|
||||
|
||||
}
|
||||
123
src/collection/collectionfilterwidget.h
Normal file
123
src/collection/collectionfilterwidget.h
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONFILTERWIDGET_H
|
||||
#define COLLECTIONFILTERWIDGET_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
|
||||
class GroupByDialog;
|
||||
class SettingsDialog;
|
||||
class Ui_CollectionFilterWidget;
|
||||
|
||||
struct QueryOptions;
|
||||
|
||||
class QMenu;
|
||||
class QActionGroup;
|
||||
class QSignalMapper;
|
||||
|
||||
class CollectionFilterWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionFilterWidget(QWidget *parent = nullptr);
|
||||
~CollectionFilterWidget();
|
||||
|
||||
static const int kFilterDelay = 500; // msec
|
||||
|
||||
enum DelayBehaviour {
|
||||
AlwaysInstant,
|
||||
DelayedOnLargeLibraries,
|
||||
AlwaysDelayed,
|
||||
};
|
||||
|
||||
static QActionGroup *CreateGroupByActions(QObject *parent);
|
||||
|
||||
void UpdateGroupByActions();
|
||||
void SetFilterHint(const QString &hint);
|
||||
void SetApplyFilterToCollection(bool filter_applies_to_model) { filter_applies_to_model_ = filter_applies_to_model; }
|
||||
void SetDelayBehaviour(DelayBehaviour behaviour) { delay_behaviour_ = behaviour; }
|
||||
void SetAgeFilterEnabled(bool enabled);
|
||||
void SetGroupByEnabled(bool enabled);
|
||||
void ShowInCollection(const QString &search);
|
||||
|
||||
QMenu *menu() const { return collection_menu_; }
|
||||
void AddMenuAction(QAction *action);
|
||||
|
||||
void SetSettingsGroup(const QString &group) { settings_group_ = group; }
|
||||
void SetCollectionModel(CollectionModel *model);
|
||||
|
||||
public slots:
|
||||
void SetQueryMode(QueryOptions::QueryMode view);
|
||||
void FocusOnFilter(QKeyEvent *e);
|
||||
|
||||
signals:
|
||||
void UpPressed();
|
||||
void DownPressed();
|
||||
void ReturnPressed();
|
||||
void Filter(const QString &text);
|
||||
|
||||
protected:
|
||||
void keyReleaseEvent(QKeyEvent *e);
|
||||
|
||||
private slots:
|
||||
void GroupingChanged(const CollectionModel::Grouping &g);
|
||||
void GroupByClicked(QAction *action);
|
||||
void SaveGroupBy();
|
||||
void ShowGroupingManager();
|
||||
|
||||
void FilterTextChanged(const QString &text);
|
||||
void FilterDelayTimeout();
|
||||
|
||||
private:
|
||||
static QAction *CreateGroupByAction(const QString &text, QObject *parent, const CollectionModel::Grouping &grouping);
|
||||
void CheckCurrentGrouping(const CollectionModel::Grouping &g);
|
||||
|
||||
private:
|
||||
Ui_CollectionFilterWidget *ui_;
|
||||
CollectionModel *model_;
|
||||
|
||||
std::unique_ptr<GroupByDialog> group_by_dialog_;
|
||||
std::unique_ptr<SavedGroupingManager> groupings_manager_;
|
||||
SettingsDialog *settings_dialog_;
|
||||
|
||||
QMenu *filter_age_menu_;
|
||||
QMenu *group_by_menu_;
|
||||
QMenu *collection_menu_;
|
||||
QActionGroup *group_by_group_;
|
||||
QSignalMapper *filter_age_mapper_;
|
||||
|
||||
QTimer *filter_delay_;
|
||||
|
||||
bool filter_applies_to_model_;
|
||||
DelayBehaviour delay_behaviour_;
|
||||
|
||||
QString settings_group_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTERWIDGET_H
|
||||
|
||||
121
src/collection/collectionfilterwidget.ui
Normal file
121
src/collection/collectionfilterwidget.ui
Normal file
@@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CollectionFilterWidget</class>
|
||||
<widget class="QWidget" name="CollectionFilterWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>30</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSearchField" name="filter" native="true">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Prefix a word with a field name to limit the search to that field, e.g. <span style=" font-weight:600;">artist:</span><span style=" font-style:italic;">Bode</span> searches the collection for all artists that contain the word Bode.</p><p><span style=" font-weight:600;">Available fields: </span><span style=" font-style:italic;">%1</span>.</p></body></html></string>
|
||||
</property>
|
||||
<property name="placeholderText" stdset="0">
|
||||
<string>Enter search terms here</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="options">
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::InstantPopup</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<action name="filter_age_all">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Entire collection</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_today">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added today</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_week">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this week</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_three_months">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added within three months</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Added within three months</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_year">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this year</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="filter_age_month">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Added this month</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="save_grouping">
|
||||
<property name="text">
|
||||
<string>Save current grouping</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="manage_groupings">
|
||||
<property name="text">
|
||||
<string>Manage saved groupings</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QSearchField</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>3rdparty/qocoa/qsearchfield.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
58
src/collection/collectionitem.h
Normal file
58
src/collection/collectionitem.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONITEM_H
|
||||
#define COLLECTIONITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
#include "core/simpletreeitem.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionItem : public SimpleTreeItem<CollectionItem> {
|
||||
public:
|
||||
enum Type {
|
||||
Type_Root,
|
||||
Type_Divider,
|
||||
Type_Container,
|
||||
Type_Song,
|
||||
Type_PlaylistContainer,
|
||||
Type_LoadingIndicator,
|
||||
};
|
||||
|
||||
CollectionItem(SimpleTreeModel<CollectionItem> *model)
|
||||
: SimpleTreeItem<CollectionItem>(Type_Root, model),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
CollectionItem(Type type, CollectionItem *parent = nullptr)
|
||||
: SimpleTreeItem<CollectionItem>(type, parent),
|
||||
container_level(-1),
|
||||
compilation_artist_node_(nullptr) {}
|
||||
|
||||
int container_level;
|
||||
Song metadata;
|
||||
CollectionItem *compilation_artist_node_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONITEM_H
|
||||
1522
src/collection/collectionmodel.cpp
Normal file
1522
src/collection/collectionmodel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
284
src/collection/collectionmodel.h
Normal file
284
src/collection/collectionmodel.h
Normal file
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONMODEL_H
|
||||
#define COLLECTIONMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QIcon>
|
||||
#include <QNetworkDiskCache>
|
||||
|
||||
#include "collectionitem.h"
|
||||
#include "collectionquery.h"
|
||||
#include "collectionwatcher.h"
|
||||
#include "sqlrow.h"
|
||||
#include "core/simpletreemodel.h"
|
||||
#include "core/song.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "engine/engine_fwd.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
|
||||
class Application;
|
||||
class AlbumCoverLoader;
|
||||
class CollectionDirectoryModel;
|
||||
class CollectionBackend;
|
||||
|
||||
class QSettings;
|
||||
|
||||
class CollectionModel : public SimpleTreeModel<CollectionItem> {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(GroupBy);
|
||||
|
||||
public:
|
||||
CollectionModel(CollectionBackend *backend, Application *app, QObject *parent = nullptr);
|
||||
~CollectionModel();
|
||||
|
||||
static const char *kSavedGroupingsSettingsGroup;
|
||||
|
||||
static const int kPrettyCoverSize;
|
||||
static const qint64 kIconCacheSize;
|
||||
|
||||
enum Role {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_ContainerType,
|
||||
Role_SortText,
|
||||
Role_Key,
|
||||
Role_Artist,
|
||||
Role_IsDivider,
|
||||
Role_Editable,
|
||||
LastRole
|
||||
};
|
||||
|
||||
// These values get saved in QSettings - don't change them
|
||||
enum GroupBy {
|
||||
GroupBy_None = 0,
|
||||
GroupBy_Artist = 1,
|
||||
GroupBy_Album = 2,
|
||||
GroupBy_YearAlbum = 3,
|
||||
GroupBy_Year = 4,
|
||||
GroupBy_Composer = 5,
|
||||
GroupBy_Genre = 6,
|
||||
GroupBy_AlbumArtist = 7,
|
||||
GroupBy_FileType = 8,
|
||||
GroupBy_Performer = 9,
|
||||
GroupBy_Grouping = 10,
|
||||
GroupBy_Bitrate = 11,
|
||||
GroupBy_Disc = 12,
|
||||
GroupBy_OriginalYearAlbum = 13,
|
||||
GroupBy_OriginalYear = 14,
|
||||
};
|
||||
|
||||
struct Grouping {
|
||||
Grouping(GroupBy f = GroupBy_None, GroupBy s = GroupBy_None, GroupBy t = GroupBy_None)
|
||||
: first(f), second(s), third(t) {}
|
||||
|
||||
GroupBy first;
|
||||
GroupBy second;
|
||||
GroupBy third;
|
||||
|
||||
const GroupBy &operator[](int i) const;
|
||||
GroupBy &operator[](int i);
|
||||
bool operator==(const Grouping &other) const {
|
||||
return first == other.first && second == other.second && third == other.third;
|
||||
}
|
||||
bool operator!=(const Grouping &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
struct QueryResult {
|
||||
QueryResult() : create_va(false) {}
|
||||
|
||||
SqlRowList rows;
|
||||
bool create_va;
|
||||
};
|
||||
|
||||
CollectionBackend *backend() const { return backend_; }
|
||||
CollectionDirectoryModel *directory_model() const { return dir_model_; }
|
||||
|
||||
// Call before Init()
|
||||
void set_show_various_artists(bool show_various_artists) { show_various_artists_ = show_various_artists; }
|
||||
|
||||
// Get information about the collection
|
||||
void GetChildSongs(CollectionItem *item, QList<QUrl> *urls, SongList *songs, QSet<int> *song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex &index) const;
|
||||
SongList GetChildSongs(const QModelIndexList &indexes) const;
|
||||
|
||||
// Might be accurate
|
||||
int total_song_count() const { return total_song_count_; }
|
||||
int total_artist_count() const { return total_artist_count_; }
|
||||
int total_album_count() const { return total_album_count_; }
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const;
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
|
||||
// Whether or not to use album cover art, if it exists, in the collection view
|
||||
void set_pretty_covers(bool use_pretty_covers);
|
||||
bool use_pretty_covers() const { return use_pretty_covers_; }
|
||||
|
||||
// Whether or not to show letters heading in the collection view
|
||||
void set_show_dividers(bool show_dividers);
|
||||
|
||||
// Save the current grouping
|
||||
void SaveGrouping(QString name);
|
||||
|
||||
// Utility functions for manipulating text
|
||||
static QString TextOrUnknown(const QString &text);
|
||||
static QString PrettyYearAlbum(int year, const QString &album);
|
||||
static QString SortText(QString text);
|
||||
static QString SortTextForNumber(int year);
|
||||
static QString SortTextForArtist(QString artist);
|
||||
static QString SortTextForSong(const Song &song);
|
||||
static QString SortTextForYear(int year);
|
||||
static QString SortTextForBitrate(int bitrate);
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void GroupingChanged(const CollectionModel::Grouping &g);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(int age);
|
||||
void SetFilterText(const QString &text);
|
||||
void SetFilterQueryMode(QueryOptions::QueryMode query_mode);
|
||||
|
||||
void SetGroupBy(const CollectionModel::Grouping &g);
|
||||
const CollectionModel::Grouping &GetGroupBy() const { return group_by_; }
|
||||
void Init(bool async = true);
|
||||
void Reset();
|
||||
void ResetAsync();
|
||||
|
||||
protected:
|
||||
void LazyPopulate(CollectionItem *item) { LazyPopulate(item, true); }
|
||||
void LazyPopulate(CollectionItem *item, bool signal);
|
||||
|
||||
private slots:
|
||||
// From CollectionBackend
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsSlightlyChanged(const SongList &songs);
|
||||
void TotalSongCountUpdatedSlot(int count);
|
||||
void TotalArtistCountUpdatedSlot(int count);
|
||||
void TotalAlbumCountUpdatedSlot(int count);
|
||||
|
||||
// Called after ResetAsync
|
||||
void ResetAsyncQueryFinished(QFuture<CollectionModel::QueryResult> future);
|
||||
|
||||
void AlbumArtLoaded(quint64 id, const QImage &image);
|
||||
|
||||
private:
|
||||
// Provides some optimisations for loading the list of items in the root.
|
||||
// This gets called a lot when filtering the playlist, so it's nice to be
|
||||
// able to do it in a background thread.
|
||||
QueryResult RunQuery(CollectionItem *parent);
|
||||
void PostQuery(CollectionItem *parent, const QueryResult &result, bool signal);
|
||||
|
||||
bool HasCompilations(const CollectionQuery &query);
|
||||
|
||||
void BeginReset();
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Collection
|
||||
// constructs a database query to populate the items. Filters are added
|
||||
// for each parent item, restricting the songs returned to a particular
|
||||
// album or artist for example.
|
||||
static void InitQuery(GroupBy type, CollectionQuery *q);
|
||||
void FilterQuery(GroupBy type, CollectionItem *item, CollectionQuery *q);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a
|
||||
// node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
CollectionItem *ItemFromQuery(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const SqlRow &row, int container_level);
|
||||
CollectionItem *ItemFromSong(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, const Song &s, int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
CollectionItem *CreateCompilationArtistNode(bool signal, CollectionItem *parent);
|
||||
|
||||
// Smart playlists are shown in another top-level node
|
||||
|
||||
void ItemFromSmartPlaylist(const QSettings &s, bool notify) const;
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
CollectionItem *InitItem(GroupBy type, bool signal, CollectionItem *parent, int container_level);
|
||||
void FinishItem(GroupBy type, bool signal, bool create_divider, CollectionItem *parent, CollectionItem *item);
|
||||
|
||||
QString DividerKey(GroupBy type, CollectionItem *item) const;
|
||||
QString DividerDisplayText(GroupBy type, const QString &key) const;
|
||||
|
||||
// Helpers
|
||||
QString AlbumIconPixmapCacheKey(const QModelIndex &index) const;
|
||||
QVariant AlbumIcon(const QModelIndex &index);
|
||||
QVariant data(const CollectionItem *item, int role) const;
|
||||
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
|
||||
|
||||
private:
|
||||
CollectionBackend *backend_;
|
||||
Application *app_;
|
||||
CollectionDirectoryModel *dir_model_;
|
||||
bool show_various_artists_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
QueryOptions query_options_;
|
||||
Grouping group_by_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, CollectionItem*> song_nodes_;
|
||||
|
||||
// Keyed on whatever the key is for that level - artist, album, year, etc.
|
||||
QMap<QString, CollectionItem*> container_nodes_[3];
|
||||
|
||||
// Keyed on a letter, a year, a century, etc.
|
||||
QMap<QString, CollectionItem*> divider_nodes_;
|
||||
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
// used as a generic icon to show when no cover art is found,
|
||||
// fixed to the same size as the artwork (32x32)
|
||||
QPixmap no_cover_icon_;
|
||||
QIcon playlists_dir_icon_;
|
||||
QIcon playlist_icon_;
|
||||
|
||||
QNetworkDiskCache *icon_cache_;
|
||||
|
||||
int init_task_id_;
|
||||
|
||||
bool use_pretty_covers_;
|
||||
bool show_dividers_;
|
||||
|
||||
AlbumCoverLoaderOptions cover_loader_options_;
|
||||
|
||||
typedef QPair<CollectionItem*, QString> ItemAndCacheKey;
|
||||
QMap<quint64, ItemAndCacheKey> pending_art_;
|
||||
QSet<QString> pending_cache_keys_;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(CollectionModel::Grouping);
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const CollectionModel::Grouping &g);
|
||||
QDataStream &operator>>(QDataStream &s, CollectionModel::Grouping &g);
|
||||
|
||||
#endif // COLLECTIONMODEL_H
|
||||
58
src/collection/collectionplaylistitem.cpp
Normal file
58
src/collection/collectionplaylistitem.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectionplaylistitem.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const QString &type)
|
||||
: PlaylistItem(type) {}
|
||||
|
||||
CollectionPlaylistItem::CollectionPlaylistItem(const Song &song)
|
||||
: PlaylistItem("Collection"), song_(song) {}
|
||||
|
||||
QUrl CollectionPlaylistItem::Url() const { return song_.url(); }
|
||||
|
||||
void CollectionPlaylistItem::Reload() {
|
||||
TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
}
|
||||
|
||||
bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||
// Rows from the songs tables come first
|
||||
song_.InitFromQuery(query, true);
|
||||
|
||||
return song_.is_valid();
|
||||
}
|
||||
|
||||
QVariant CollectionPlaylistItem::DatabaseValue(DatabaseColumn column) const {
|
||||
switch (column) {
|
||||
case Column_CollectionId: return song_.id();
|
||||
default: return PlaylistItem::DatabaseValue(column);
|
||||
}
|
||||
}
|
||||
|
||||
Song CollectionPlaylistItem::Metadata() const {
|
||||
if (HasTemporaryMetadata()) return temp_metadata_;
|
||||
return song_;
|
||||
}
|
||||
|
||||
52
src/collection/collectionplaylistitem.h
Normal file
52
src/collection/collectionplaylistitem.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONPLAYLISTITEM_H
|
||||
#define COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "core/song.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
class CollectionPlaylistItem : public PlaylistItem {
|
||||
public:
|
||||
CollectionPlaylistItem(const QString &type);
|
||||
CollectionPlaylistItem(const Song &song);
|
||||
|
||||
bool InitFromQuery(const SqlRow &query);
|
||||
void Reload();
|
||||
|
||||
Song Metadata() const;
|
||||
void SetMetadata(const Song &song) { song_ = song; }
|
||||
|
||||
QUrl Url() const;
|
||||
|
||||
bool IsLocalCollectionItem() const { return true; }
|
||||
|
||||
protected:
|
||||
QVariant DatabaseValue(DatabaseColumn column) const;
|
||||
|
||||
protected:
|
||||
Song song_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONPLAYLISTITEM_H
|
||||
|
||||
204
src/collection/collectionquery.cpp
Normal file
204
src/collection/collectionquery.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectionquery.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QDateTime>
|
||||
#include <QSqlError>
|
||||
|
||||
QueryOptions::QueryOptions() : max_age_(-1), query_mode_(QueryMode_All) {}
|
||||
|
||||
CollectionQuery::CollectionQuery(const QueryOptions& options)
|
||||
: include_unavailable_(false), join_with_fts_(false), limit_(-1) {
|
||||
|
||||
if (!options.filter().isEmpty()) {
|
||||
// We need to munge the filter text a little bit to get it to work as
|
||||
// expected with sqlite's FTS3:
|
||||
// 1) Append * to all tokens.
|
||||
// 2) Prefix "fts" to column names.
|
||||
// 3) Remove colons which don't correspond to column names.
|
||||
|
||||
// Split on whitespace
|
||||
QStringList tokens(options.filter().split(QRegExp("\\s+"), QString::SkipEmptyParts));
|
||||
QString query;
|
||||
for (QString token : tokens) {
|
||||
token.remove('(');
|
||||
token.remove(')');
|
||||
token.remove('"');
|
||||
token.replace('-', ' ');
|
||||
|
||||
if (token.contains(':')) {
|
||||
// Only prefix fts if the token is a valid column name.
|
||||
if (Song::kFtsColumns.contains("fts" + token.section(':', 0, 0),
|
||||
Qt::CaseInsensitive)) {
|
||||
// Account for multiple colons.
|
||||
QString columntoken = token.section(':', 0, 0, QString::SectionIncludeTrailingSep);
|
||||
QString subtoken = token.section(':', 1, -1);
|
||||
subtoken.replace(":", " ");
|
||||
subtoken = subtoken.trimmed();
|
||||
query += "fts" + columntoken + subtoken + "* ";
|
||||
}
|
||||
else {
|
||||
token.replace(":", " ");
|
||||
token = token.trimmed();
|
||||
query += token + "* ";
|
||||
}
|
||||
}
|
||||
else {
|
||||
query += token + "* ";
|
||||
}
|
||||
}
|
||||
|
||||
where_clauses_ << "fts.%fts_table_noprefix MATCH ?";
|
||||
bound_values_ << query;
|
||||
join_with_fts_ = true;
|
||||
}
|
||||
|
||||
if (options.max_age() != -1) {
|
||||
int cutoff = QDateTime::currentDateTime().toTime_t() - options.max_age();
|
||||
|
||||
where_clauses_ << "ctime > ?";
|
||||
bound_values_ << cutoff;
|
||||
}
|
||||
|
||||
// TODO: currently you cannot use any QueryMode other than All and fts at the
|
||||
// same time.
|
||||
// joining songs, duplicated_songs and songs_fts all together takes a huge
|
||||
// amount of
|
||||
// time. the query takes about 20 seconds on my machine then. why?
|
||||
// untagged mode could work with additional filtering but I'm disabling it
|
||||
// just to be
|
||||
// consistent - this way filtering is available only in the All mode.
|
||||
// remember though that when you fix the Duplicates + FTS cooperation, enable
|
||||
// the filtering in both Duplicates and Untagged modes.
|
||||
duplicates_only_ = options.query_mode() == QueryOptions::QueryMode_Duplicates;
|
||||
|
||||
if (options.query_mode() == QueryOptions::QueryMode_Untagged) {
|
||||
where_clauses_ << "(artist = '' OR album = '' OR title ='')";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString CollectionQuery::GetInnerQuery() {
|
||||
return duplicates_only_
|
||||
? QString(" INNER JOIN (select * from duplicated_songs) dsongs "
|
||||
"ON (%songs_table.artist = dsongs.dup_artist "
|
||||
"AND %songs_table.album = dsongs.dup_album "
|
||||
"AND %songs_table.title = dsongs.dup_title) ")
|
||||
: QString();
|
||||
}
|
||||
|
||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||
|
||||
// ignore 'literal' for IN
|
||||
if (!op.compare("IN", Qt::CaseInsensitive)) {
|
||||
QStringList final;
|
||||
for (const QString& single_value : value.toStringList()) {
|
||||
final.append("?");
|
||||
bound_values_ << single_value;
|
||||
}
|
||||
|
||||
where_clauses_ << QString("%1 IN (" + final.join(",") + ")").arg(column);
|
||||
}
|
||||
else {
|
||||
// Do integers inline - sqlite seems to get confused when you pass integers
|
||||
// to bound parameters
|
||||
if (value.type() == QVariant::Int) {
|
||||
where_clauses_ << QString("%1 %2 %3").arg(column, op, value.toString());
|
||||
}
|
||||
else {
|
||||
where_clauses_ << QString("%1 %2 ?").arg(column, op);
|
||||
bound_values_ << value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionQuery::AddCompilationRequirement(bool compilation) {
|
||||
// The unary + is added to prevent sqlite from using the index
|
||||
// idx_comp_artist. When joining with fts, sqlite 3.8 has a tendency
|
||||
// to use this index and thereby nesting the tables in an order
|
||||
// which gives very poor performance
|
||||
|
||||
where_clauses_ << QString("+compilation_effective = %1").arg(compilation ? 1 : 0);
|
||||
|
||||
}
|
||||
|
||||
QSqlQuery CollectionQuery::Exec(QSqlDatabase db, const QString &songs_table, const QString &fts_table) {
|
||||
|
||||
QString sql;
|
||||
|
||||
if (join_with_fts_) {
|
||||
sql = QString("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table, fts_table);
|
||||
}
|
||||
else {
|
||||
sql = QString("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table, GetInnerQuery());
|
||||
}
|
||||
|
||||
QStringList where_clauses(where_clauses_);
|
||||
if (!include_unavailable_) {
|
||||
where_clauses << "unavailable = 0";
|
||||
}
|
||||
|
||||
if (!where_clauses.isEmpty()) sql += " WHERE " + where_clauses.join(" AND ");
|
||||
|
||||
if (!order_by_.isEmpty()) sql += " ORDER BY " + order_by_;
|
||||
|
||||
if (limit_ != -1) sql += " LIMIT " + QString::number(limit_);
|
||||
|
||||
sql.replace("%songs_table", songs_table);
|
||||
sql.replace("%fts_table_noprefix", fts_table.section('.', -1, -1));
|
||||
sql.replace("%fts_table", fts_table);
|
||||
|
||||
query_ = QSqlQuery(db);
|
||||
query_.prepare(sql);
|
||||
|
||||
// Bind values
|
||||
for (const QVariant& value : bound_values_) {
|
||||
query_.addBindValue(value);
|
||||
}
|
||||
|
||||
query_.exec();
|
||||
return query_;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionQuery::Next() { return query_.next(); }
|
||||
|
||||
QVariant CollectionQuery::Value(int column) const { return query_.value(column); }
|
||||
|
||||
bool QueryOptions::Matches(const Song &song) const {
|
||||
|
||||
if (max_age_ != -1) {
|
||||
const uint cutoff = QDateTime::currentDateTime().toTime_t() - max_age_;
|
||||
if (song.ctime() <= cutoff) return false;
|
||||
}
|
||||
|
||||
if (!filter_.isNull()) {
|
||||
return song.artist().contains(filter_, Qt::CaseInsensitive) || song.album().contains(filter_, Qt::CaseInsensitive) || song.title().contains(filter_, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
116
src/collection/collectionquery.h
Normal file
116
src/collection/collectionquery.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONQUERY_H
|
||||
#define COLLECTIONQUERY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QSqlQuery>
|
||||
#include <QStringList>
|
||||
#include <QVariantList>
|
||||
|
||||
class Song;
|
||||
class CollectionBackend;
|
||||
|
||||
// This structure let's you customize behaviour of any CollectionQuery.
|
||||
struct QueryOptions {
|
||||
// Modes of CollectionQuery:
|
||||
// - use the all songs table
|
||||
// - use the duplicated songs view; by duplicated we mean those songs
|
||||
// for which the (artist, album, title) tuple is found more than once
|
||||
// in the songs table
|
||||
// - use the untagged songs view; by untagged we mean those for which
|
||||
// at least one of the (artist, album, title) tags is empty
|
||||
// Please note that additional filtering based on fts table (the filter
|
||||
// attribute) won't work in Duplicates and Untagged modes.
|
||||
enum QueryMode {
|
||||
QueryMode_All,
|
||||
QueryMode_Duplicates,
|
||||
QueryMode_Untagged
|
||||
};
|
||||
|
||||
QueryOptions();
|
||||
|
||||
bool Matches(const Song &song) const;
|
||||
|
||||
QString filter() const { return filter_; }
|
||||
void set_filter(const QString &filter) {
|
||||
this->filter_ = filter;
|
||||
this->query_mode_ = QueryMode_All;
|
||||
}
|
||||
|
||||
int max_age() const { return max_age_; }
|
||||
void set_max_age(int max_age) { this->max_age_ = max_age; }
|
||||
|
||||
QueryMode query_mode() const { return query_mode_; }
|
||||
void set_query_mode(QueryMode query_mode) {
|
||||
this->query_mode_ = query_mode;
|
||||
this->filter_ = QString();
|
||||
}
|
||||
|
||||
private:
|
||||
QString filter_;
|
||||
int max_age_;
|
||||
QueryMode query_mode_;
|
||||
};
|
||||
|
||||
class CollectionQuery {
|
||||
public:
|
||||
CollectionQuery(const QueryOptions &options = QueryOptions());
|
||||
|
||||
// Sets contents of SELECT clause on the query (list of columns to get).
|
||||
void SetColumnSpec(const QString &spec) { column_spec_ = spec; }
|
||||
// Sets an ORDER BY clause on the query.
|
||||
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
||||
|
||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all
|
||||
// the fragments with AND operator.
|
||||
// Please note that IN operator expects a QStringList as value.
|
||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||
|
||||
void AddCompilationRequirement(bool compilation);
|
||||
void SetLimit(int limit) { limit_ = limit; }
|
||||
void SetIncludeUnavailable(bool include_unavailable) { include_unavailable_ = include_unavailable; }
|
||||
|
||||
QSqlQuery Exec(QSqlDatabase db, const QString &songs_table, const QString &fts_table);
|
||||
bool Next();
|
||||
QVariant Value(int column) const;
|
||||
|
||||
operator const QSqlQuery &() const { return query_; }
|
||||
|
||||
private:
|
||||
QString GetInnerQuery();
|
||||
|
||||
bool include_unavailable_;
|
||||
bool join_with_fts_;
|
||||
QString column_spec_;
|
||||
QString order_by_;
|
||||
QStringList where_clauses_;
|
||||
QVariantList bound_values_;
|
||||
int limit_;
|
||||
bool duplicates_only_;
|
||||
|
||||
QSqlQuery query_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONQUERY_H
|
||||
716
src/collection/collectionview.cpp
Normal file
716
src/collection/collectionview.cpp
Normal file
@@ -0,0 +1,716 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectionview.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QHelpEvent>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QToolTip>
|
||||
#include <QWhatsThis>
|
||||
|
||||
#include "collectiondirectorymodel.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionitem.h"
|
||||
#include "collectionbackend.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#ifdef HAVE_GSTREAMER
|
||||
#include "dialogs/organisedialog.h"
|
||||
#include "dialogs/organiseerrordialog.h"
|
||||
#endif
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
CollectionItemDelegate::CollectionItemDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
|
||||
|
||||
void CollectionItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const {
|
||||
|
||||
const bool is_divider = index.data(CollectionModel::Role_IsDivider).toBool();
|
||||
|
||||
if (is_divider) {
|
||||
QString text(index.data().toString());
|
||||
|
||||
painter->save();
|
||||
|
||||
QRect text_rect(opt.rect);
|
||||
|
||||
// Does this item have an icon?
|
||||
QPixmap pixmap;
|
||||
QVariant decoration = index.data(Qt::DecorationRole);
|
||||
if (!decoration.isNull()) {
|
||||
if (decoration.canConvert<QPixmap>()) {
|
||||
pixmap = decoration.value<QPixmap>();
|
||||
}
|
||||
else if (decoration.canConvert<QIcon>()) {
|
||||
pixmap = decoration.value<QIcon>().pixmap(opt.decorationSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the icon at the left of the text rectangle
|
||||
if (!pixmap.isNull()) {
|
||||
QRect icon_rect(text_rect.topLeft(), opt.decorationSize);
|
||||
const int padding = (text_rect.height() - icon_rect.height()) / 2;
|
||||
icon_rect.adjust(padding, padding, padding, padding);
|
||||
text_rect.moveLeft(icon_rect.right() + padding + 6);
|
||||
|
||||
if (pixmap.size() != opt.decorationSize) {
|
||||
pixmap = pixmap.scaled(opt.decorationSize, Qt::KeepAspectRatio);
|
||||
}
|
||||
|
||||
painter->drawPixmap(icon_rect, pixmap);
|
||||
}
|
||||
else {
|
||||
text_rect.setLeft(text_rect.left() + 30);
|
||||
}
|
||||
|
||||
// Draw the text
|
||||
QFont bold_font(opt.font);
|
||||
bold_font.setBold(true);
|
||||
|
||||
painter->setPen(opt.palette.color(QPalette::Text));
|
||||
painter->setFont(bold_font);
|
||||
painter->drawText(text_rect, text);
|
||||
|
||||
// Draw the line under the item
|
||||
QColor line_color = opt.palette.color(QPalette::Text);
|
||||
QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight());
|
||||
const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width();
|
||||
line_color.setAlphaF(0.0);
|
||||
grad_color.setColorAt(0, line_color);
|
||||
line_color.setAlphaF(0.5);
|
||||
grad_color.setColorAt(fade_start_end, line_color);
|
||||
grad_color.setColorAt(1.0 - fade_start_end, line_color);
|
||||
line_color.setAlphaF(0.0);
|
||||
grad_color.setColorAt(1, line_color);
|
||||
painter->setPen(QPen(grad_color, 1));
|
||||
painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
else {
|
||||
QStyledItemDelegate::paint(painter, opt, index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) {
|
||||
|
||||
Q_UNUSED(option);
|
||||
|
||||
if (!event || !view) return false;
|
||||
|
||||
QHelpEvent *he = static_cast<QHelpEvent*>(event);
|
||||
QString text = displayText(index.data(), QLocale::system());
|
||||
|
||||
if (text.isEmpty() || !he) return false;
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::ToolTip: {
|
||||
QRect displayed_text;
|
||||
QSize real_text;
|
||||
bool is_elided = false;
|
||||
|
||||
real_text = sizeHint(option, index);
|
||||
displayed_text = view->visualRect(index);
|
||||
is_elided = displayed_text.width() < real_text.width();
|
||||
|
||||
if (is_elided) {
|
||||
QToolTip::showText(he->globalPos(), text, view);
|
||||
}
|
||||
else if (index.data(Qt::ToolTipRole).isValid()) {
|
||||
// If the item has a tooltip text, display it
|
||||
QString tooltip_text = index.data(Qt::ToolTipRole).toString();
|
||||
QToolTip::showText(he->globalPos(), tooltip_text, view);
|
||||
}
|
||||
else {
|
||||
// in case that another text was previously displayed
|
||||
QToolTip::hideText();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::QueryWhatsThis:
|
||||
return true;
|
||||
|
||||
case QEvent::WhatsThis:
|
||||
QWhatsThis::showText(he->globalPos(), text, view);
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
CollectionView::CollectionView(QWidget *parent)
|
||||
: AutoExpandingTreeView(parent),
|
||||
app_(nullptr),
|
||||
filter_(nullptr),
|
||||
total_song_count_(-1),
|
||||
total_artist_count_(-1),
|
||||
total_album_count_(-1),
|
||||
nomusic_(":/pictures/nomusic.png"),
|
||||
context_menu_(nullptr),
|
||||
is_in_keyboard_search_(false)
|
||||
{
|
||||
|
||||
setItemDelegate(new CollectionItemDelegate(this));
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setHeaderHidden(true);
|
||||
setAllColumnsShowFocus(true);
|
||||
setDragEnabled(true);
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
|
||||
setStyleSheet("QTreeView::item{padding-top:1px;}");
|
||||
|
||||
}
|
||||
|
||||
CollectionView::~CollectionView() {}
|
||||
|
||||
void CollectionView::SaveFocus() {
|
||||
|
||||
QModelIndex current = currentIndex();
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Song || type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_selected_path_.clear();
|
||||
last_selected_song_ = Song();
|
||||
last_selected_container_ = QString();
|
||||
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song: {
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
if (!songs.isEmpty()) {
|
||||
last_selected_song_ = songs.last();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider: {
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_container_ = text;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
SaveContainerPath(current);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SaveContainerPath(const QModelIndex &child) {
|
||||
|
||||
QModelIndex current = model()->parent(child);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
if (!type.isValid() || !(type.toInt() == CollectionItem::Type_Container || type.toInt() == CollectionItem::Type_Divider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
last_selected_path_ << text;
|
||||
SaveContainerPath(current);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::RestoreFocus() {
|
||||
|
||||
if (last_selected_container_.isEmpty() && last_selected_song_.url().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
RestoreLevelFocus();
|
||||
|
||||
}
|
||||
|
||||
bool CollectionView::RestoreLevelFocus(const QModelIndex &parent) {
|
||||
|
||||
if (model()->canFetchMore(parent)) {
|
||||
model()->fetchMore(parent);
|
||||
}
|
||||
int rows = model()->rowCount(parent);
|
||||
for (int i = 0; i < rows; i++) {
|
||||
QModelIndex current = model()->index(i, 0, parent);
|
||||
QVariant type = model()->data(current, CollectionModel::Role_Type);
|
||||
switch (type.toInt()) {
|
||||
case CollectionItem::Type_Song:
|
||||
if (!last_selected_song_.url().isEmpty()) {
|
||||
QModelIndex index = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(current);
|
||||
SongList songs = app_->collection_model()->GetChildSongs(index);
|
||||
for (const Song& song : songs) {
|
||||
if (song == last_selected_song_) {
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CollectionItem::Type_Container:
|
||||
case CollectionItem::Type_Divider: {
|
||||
QString text = model()->data(current, CollectionModel::Role_SortText).toString();
|
||||
if (!last_selected_container_.isEmpty() && last_selected_container_ == text) {
|
||||
emit expand(current);
|
||||
setCurrentIndex(current);
|
||||
return true;
|
||||
}
|
||||
else if (last_selected_path_.contains(text)) {
|
||||
emit expand(current);
|
||||
// If a selected container or song were not found, we've got into a wrong subtree
|
||||
// (happens with "unknown" all the time)
|
||||
if (!RestoreLevelFocus(current)) {
|
||||
emit collapse(current);
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::ReloadSettings() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QSettings settings;
|
||||
|
||||
settings.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
SetAutoOpen(settings.value("auto_open", true).toBool());
|
||||
|
||||
if (app_ != nullptr) {
|
||||
app_->collection_model()->set_pretty_covers(settings.value("pretty_covers", true).toBool());
|
||||
app_->collection_model()->set_show_dividers(settings.value("show_dividers", true).toBool());
|
||||
}
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetApplication(Application *app) {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
app_ = app;
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::SetFilter(CollectionFilterWidget *filter) { filter_ = filter; }
|
||||
|
||||
void CollectionView::TotalSongCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_song_count_;
|
||||
total_song_count_ = count;
|
||||
if (old != total_song_count_) update();
|
||||
|
||||
if (total_song_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalSongCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::TotalArtistCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_artist_count_;
|
||||
total_artist_count_ = count;
|
||||
if (old != total_artist_count_) update();
|
||||
|
||||
if (total_artist_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalArtistCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::TotalAlbumCountUpdated(int count) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__ << count;
|
||||
|
||||
bool old = total_album_count_;
|
||||
total_album_count_ = count;
|
||||
if (old != total_album_count_) update();
|
||||
|
||||
if (total_album_count_ == 0)
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
emit TotalAlbumCountUpdated_();
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::paintEvent(QPaintEvent *event) {
|
||||
|
||||
//qLog(Debug) << __FUNCTION__;
|
||||
|
||||
if (total_song_count_ == 0) {
|
||||
QPainter p(viewport());
|
||||
QRect rect(viewport()->rect());
|
||||
|
||||
// Draw the confused strawberry
|
||||
QRect image_rect((rect.width() - nomusic_.width()) / 2, 50, nomusic_.width(), nomusic_.height());
|
||||
p.drawPixmap(image_rect, nomusic_);
|
||||
|
||||
// Draw the title text
|
||||
QFont bold_font;
|
||||
bold_font.setBold(true);
|
||||
p.setFont(bold_font);
|
||||
|
||||
QFontMetrics metrics(bold_font);
|
||||
|
||||
QRect title_rect(0, image_rect.bottom() + 20, rect.width(), metrics.height());
|
||||
p.drawText(title_rect, Qt::AlignHCenter, tr("Your collection is empty!"));
|
||||
|
||||
// Draw the other text
|
||||
p.setFont(QFont());
|
||||
|
||||
QRect text_rect(0, title_rect.bottom() + 5, rect.width(), metrics.height());
|
||||
p.drawText(text_rect, Qt::AlignHCenter, tr("Click here to add some music"));
|
||||
}
|
||||
else {
|
||||
QTreeView::paintEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::mouseReleaseEvent(QMouseEvent *e) {
|
||||
|
||||
QTreeView::mouseReleaseEvent(e);
|
||||
|
||||
if (total_song_count_ == 0) {
|
||||
emit ShowConfigDialog();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(this);
|
||||
add_to_playlist_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Append to current playlist"), this, SLOT(AddToPlaylist()));
|
||||
load_ = context_menu_->addAction(IconLoader::Load("media-play"), tr("Replace current playlist"), this, SLOT(Load()));
|
||||
open_in_new_playlist_ = context_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
add_to_playlist_enqueue_ = context_menu_->addAction(IconLoader::Load("go-next"), tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
context_menu_->addSeparator();
|
||||
organise_ = context_menu_->addAction(IconLoader::Load("edit-copy"), tr("Organise files..."), this, SLOT(Organise()));
|
||||
copy_to_device_ = context_menu_->addAction(IconLoader::Load("device"), tr("Copy to device..."), this, SLOT(CopyToDevice()));
|
||||
//delete_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from disk..."), this, SLOT(Delete()));
|
||||
#endif
|
||||
|
||||
context_menu_->addSeparator();
|
||||
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit track information..."), this, SLOT(EditTracks()));
|
||||
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename"), tr("Edit tracks information..."), this, SLOT(EditTracks()));
|
||||
show_in_browser_ = context_menu_->addAction(IconLoader::Load("document-open-folder"), tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
show_in_various_ = context_menu_->addAction( tr("Show in various artists"), this, SLOT(ShowInVarious()));
|
||||
no_show_in_various_ = context_menu_->addAction( tr("Don't show in various artists"), this, SLOT(NoShowInVarious()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
|
||||
context_menu_->addMenu(filter_->menu());
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
|
||||
connect(app_->device_manager()->connected_devices_model(), SIGNAL(IsEmptyChanged(bool)), copy_to_device_, SLOT(setDisabled(bool)));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
context_menu_index_ = indexAt(e->pos());
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(context_menu_index_);
|
||||
|
||||
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
|
||||
int regular_elements = 0;
|
||||
int regular_editable = 0;
|
||||
|
||||
for (const QModelIndex& index : selected_indexes) {
|
||||
regular_elements++;
|
||||
if(app_->collection_model()->data(index, CollectionModel::Role_Editable).toBool()) {
|
||||
regular_editable++;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if custom plugin actions should be enabled / visible
|
||||
//const int songs_selected = smart_playlists + smart_playlists_header + regular_elements;
|
||||
const int songs_selected = regular_elements;
|
||||
const bool regular_elements_only = songs_selected == regular_elements && regular_elements > 0;
|
||||
|
||||
// in all modes
|
||||
load_->setEnabled(songs_selected);
|
||||
add_to_playlist_->setEnabled(songs_selected);
|
||||
open_in_new_playlist_->setEnabled(songs_selected);
|
||||
add_to_playlist_enqueue_->setEnabled(songs_selected);
|
||||
|
||||
// if neither edit_track not edit_tracks are available, we show disabled edit_track element
|
||||
//edit_track_->setVisible(!smart_playlists_only && (regular_editable <= 1));
|
||||
edit_track_->setVisible(regular_editable <= 1);
|
||||
edit_track_->setEnabled(regular_editable == 1);
|
||||
|
||||
// only when no smart playlists selected
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_->setVisible(regular_elements_only);
|
||||
copy_to_device_->setVisible(regular_elements_only);
|
||||
//delete_->setVisible(regular_elements_only);
|
||||
#endif
|
||||
show_in_various_->setVisible(regular_elements_only);
|
||||
no_show_in_various_->setVisible(regular_elements_only);
|
||||
|
||||
// only when all selected items are editable
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_->setEnabled(regular_elements == regular_editable);
|
||||
copy_to_device_->setEnabled(regular_elements == regular_editable);
|
||||
//delete_->setEnabled(regular_elements == regular_editable);
|
||||
#endif
|
||||
|
||||
context_menu_->popup(e->globalPos());
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::ShowInVarious() { ShowInVarious(true); }
|
||||
|
||||
void CollectionView::NoShowInVarious() { ShowInVarious(false); }
|
||||
|
||||
void CollectionView::ShowInVarious(bool on) {
|
||||
|
||||
if (!context_menu_index_.isValid()) return;
|
||||
|
||||
// Map is from album name -> all artists sharing that album name, built from each selected
|
||||
// song. We put through "Various Artists" changes one album at a time, to make sure the old album
|
||||
// node gets removed (due to all children removed), before the new one gets added
|
||||
QMultiMap<QString, QString> albums;
|
||||
for (const Song& song : GetSelectedSongs()) {
|
||||
if (albums.find(song.album(), song.artist()) == albums.end())
|
||||
albums.insert(song.album(), song.artist());
|
||||
}
|
||||
|
||||
// If we have only one album and we are putting it into Various Artists, check to see
|
||||
// if there are other Artists in this album and prompt the user if they'd like them moved, too
|
||||
if (on && albums.keys().count() == 1) {
|
||||
const QString album = albums.keys().first();
|
||||
QList<Song> all_of_album = app_->collection_backend()->GetSongsByAlbum(album);
|
||||
QSet<QString> other_artists;
|
||||
for (const Song &s : all_of_album) {
|
||||
if (!albums.contains(album, s.artist()) &&
|
||||
!other_artists.contains(s.artist())) {
|
||||
other_artists.insert(s.artist());
|
||||
}
|
||||
}
|
||||
if (other_artists.count() > 0) {
|
||||
if (QMessageBox::question(this,
|
||||
tr("There are other songs in this album"),
|
||||
tr("Would you like to move the other songs in this album to Various Artists as well?"),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes) == QMessageBox::Yes) {
|
||||
for (const QString &s : other_artists) {
|
||||
albums.insert(album, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &album : QSet<QString>::fromList(albums.keys())) {
|
||||
app_->collection_backend()->ForceCompilation(album, albums.values(album), on);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::Load() {
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->clear_first_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::AddToPlaylist() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::AddToPlaylistEnqueue() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->enqueue_now_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::OpenInNewPlaylist() {
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData* mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->open_in_new_playlist_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::keyboardSearch(const QString &search) {
|
||||
|
||||
is_in_keyboard_search_ = true;
|
||||
QTreeView::keyboardSearch(search);
|
||||
is_in_keyboard_search_ = false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionView::scrollTo(const QModelIndex &index, ScrollHint hint) {
|
||||
|
||||
if (is_in_keyboard_search_)
|
||||
QTreeView::scrollTo(index, QAbstractItemView::PositionAtTop);
|
||||
else
|
||||
QTreeView::scrollTo(index, hint);
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionView::GetSelectedSongs() const {
|
||||
|
||||
QModelIndexList selected_indexes = qobject_cast<QSortFilterProxyModel*>(model())->mapSelectionToSource(selectionModel()->selection()).indexes();
|
||||
return app_->collection_model()->GetChildSongs(selected_indexes);
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void CollectionView::Organise() {
|
||||
|
||||
if (!organise_dialog_)
|
||||
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
|
||||
|
||||
organise_dialog_->SetDestinationModel(app_->collection_model()->directory_model());
|
||||
organise_dialog_->SetCopy(false);
|
||||
if (organise_dialog_->SetSongs(GetSelectedSongs()))
|
||||
organise_dialog_->show();
|
||||
else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("None of the selected songs were suitable for copying to a device"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void CollectionView::EditTracks() {
|
||||
|
||||
if (!edit_tag_dialog_) {
|
||||
edit_tag_dialog_.reset(new EditTagDialog(app_, this));
|
||||
}
|
||||
edit_tag_dialog_->SetSongs(GetSelectedSongs());
|
||||
edit_tag_dialog_->show();
|
||||
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void CollectionView::CopyToDevice() {
|
||||
|
||||
if (!organise_dialog_)
|
||||
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
|
||||
|
||||
organise_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organise_dialog_->SetCopy(true);
|
||||
organise_dialog_->SetSongs(GetSelectedSongs());
|
||||
organise_dialog_->show();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void CollectionView::FilterReturnPressed() {
|
||||
|
||||
if (!currentIndex().isValid()) {
|
||||
// Pick the first thing that isn't a divider
|
||||
for (int row = 0; row < model()->rowCount(); ++row) {
|
||||
QModelIndex idx(model()->index(row, 0));
|
||||
if (idx.data(CollectionModel::Role_Type) != CollectionItem::Type_Divider) {
|
||||
setCurrentIndex(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentIndex().isValid()) return;
|
||||
|
||||
emit doubleClicked(currentIndex());
|
||||
}
|
||||
|
||||
void CollectionView::ShowInBrowser() {
|
||||
QList<QUrl> urls;
|
||||
for (const Song &song : GetSelectedSongs()) {
|
||||
urls << song.url();
|
||||
}
|
||||
|
||||
Utilities::OpenInFileBrowser(urls);
|
||||
}
|
||||
|
||||
int CollectionView::TotalSongs() {
|
||||
return total_song_count_;
|
||||
}
|
||||
int CollectionView::TotalArtists() {
|
||||
return total_artist_count_;
|
||||
}
|
||||
int CollectionView::TotalAlbums() {
|
||||
return total_album_count_;
|
||||
}
|
||||
163
src/collection/collectionview.h
Normal file
163
src/collection/collectionview.h
Normal file
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONVIEW_H
|
||||
#define COLLECTIONVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "dialogs/edittagdialog.h"
|
||||
#include "widgets/autoexpandingtreeview.h"
|
||||
|
||||
class Application;
|
||||
class CollectionFilterWidget;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
class OrganiseDialog;
|
||||
#endif
|
||||
|
||||
class QMimeData;
|
||||
|
||||
class CollectionItemDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionItemDelegate(QObject *parent);
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
|
||||
public slots:
|
||||
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index);
|
||||
};
|
||||
|
||||
class CollectionView : public AutoExpandingTreeView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionView(QWidget *parent = nullptr);
|
||||
~CollectionView();
|
||||
|
||||
//static const char *kSettingsGroup;
|
||||
|
||||
// Returns Songs currently selected in the collection view. Please note that the
|
||||
// selection is recursive meaning that if for example an album is selected
|
||||
// this will return all of it's songs.
|
||||
SongList GetSelectedSongs() const;
|
||||
|
||||
void SetApplication(Application *app);
|
||||
void SetFilter(CollectionFilterWidget *filter);
|
||||
|
||||
// QTreeView
|
||||
void keyboardSearch(const QString &search);
|
||||
void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible);
|
||||
|
||||
int TotalSongs();
|
||||
int TotalArtists();
|
||||
int TotalAlbums();
|
||||
|
||||
public slots:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void TotalArtistCountUpdated(int count);
|
||||
void TotalAlbumCountUpdated(int count);
|
||||
void ReloadSettings();
|
||||
|
||||
void FilterReturnPressed();
|
||||
|
||||
void SaveFocus();
|
||||
void RestoreFocus();
|
||||
|
||||
signals:
|
||||
void ShowConfigDialog();
|
||||
|
||||
void TotalSongCountUpdated_();
|
||||
void TotalArtistCountUpdated_();
|
||||
void TotalAlbumCountUpdated_();
|
||||
|
||||
protected:
|
||||
// QWidget
|
||||
void paintEvent(QPaintEvent *event);
|
||||
void mouseReleaseEvent(QMouseEvent *e);
|
||||
void contextMenuEvent(QContextMenuEvent *e);
|
||||
|
||||
private slots:
|
||||
void Load();
|
||||
void AddToPlaylist();
|
||||
void AddToPlaylistEnqueue();
|
||||
void OpenInNewPlaylist();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void Organise();
|
||||
void CopyToDevice();
|
||||
#endif
|
||||
void EditTracks();
|
||||
void ShowInBrowser();
|
||||
void ShowInVarious();
|
||||
void NoShowInVarious();
|
||||
|
||||
private:
|
||||
void RecheckIsEmpty();
|
||||
void ShowInVarious(bool on);
|
||||
bool RestoreLevelFocus(const QModelIndex &parent = QModelIndex());
|
||||
void SaveContainerPath(const QModelIndex &child);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
CollectionFilterWidget *filter_;
|
||||
|
||||
int total_song_count_;
|
||||
int total_artist_count_;
|
||||
int total_album_count_;
|
||||
|
||||
QPixmap nomusic_;
|
||||
|
||||
QMenu *context_menu_;
|
||||
QModelIndex context_menu_index_;
|
||||
QAction *load_;
|
||||
QAction *add_to_playlist_;
|
||||
QAction *add_to_playlist_enqueue_;
|
||||
QAction *open_in_new_playlist_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
QAction *organise_;
|
||||
QAction *copy_to_device_;
|
||||
#endif
|
||||
QAction *delete_;
|
||||
QAction *edit_track_;
|
||||
QAction *edit_tracks_;
|
||||
QAction *show_in_browser_;
|
||||
QAction *show_in_various_;
|
||||
QAction *no_show_in_various_;
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
std::unique_ptr<OrganiseDialog> organise_dialog_;
|
||||
#endif
|
||||
std::unique_ptr<EditTagDialog> edit_tag_dialog_;
|
||||
|
||||
bool is_in_keyboard_search_;
|
||||
|
||||
// Save focus
|
||||
Song last_selected_song_;
|
||||
QString last_selected_container_;
|
||||
QSet<QString> last_selected_path_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONVIEW_H
|
||||
|
||||
48
src/collection/collectionviewcontainer.cpp
Normal file
48
src/collection/collectionviewcontainer.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectionviewcontainer.h"
|
||||
#include "ui_collectionviewcontainer.h"
|
||||
|
||||
CollectionViewContainer::CollectionViewContainer(QWidget *parent) : QWidget(parent), ui_(new Ui_CollectionViewContainer) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
view()->SetFilter(filter());
|
||||
|
||||
connect(filter(), SIGNAL(UpPressed()), view(), SLOT(UpAndFocus()));
|
||||
connect(filter(), SIGNAL(DownPressed()), view(), SLOT(DownAndFocus()));
|
||||
connect(filter(), SIGNAL(ReturnPressed()), view(), SLOT(FilterReturnPressed()));
|
||||
connect(view(), SIGNAL(FocusOnFilterSignal(QKeyEvent*)), filter(), SLOT(FocusOnFilter(QKeyEvent*)));
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
CollectionViewContainer::~CollectionViewContainer() { delete ui_; }
|
||||
|
||||
CollectionView* CollectionViewContainer::view() const { return ui_->view; }
|
||||
|
||||
CollectionFilterWidget *CollectionViewContainer::filter() const {
|
||||
return ui_->filter;
|
||||
}
|
||||
|
||||
void CollectionViewContainer::ReloadSettings() { view()->ReloadSettings(); }
|
||||
49
src/collection/collectionviewcontainer.h
Normal file
49
src/collection/collectionviewcontainer.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONVIEWCONTAINER_H
|
||||
#define COLLECTIONVIEWCONTAINER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class CollectionFilterWidget;
|
||||
class CollectionView;
|
||||
class Ui_CollectionViewContainer;
|
||||
|
||||
class CollectionViewContainer : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionViewContainer(QWidget *parent = nullptr);
|
||||
~CollectionViewContainer();
|
||||
|
||||
CollectionFilterWidget *filter() const;
|
||||
CollectionView *view() const;
|
||||
|
||||
void ReloadSettings();
|
||||
|
||||
private:
|
||||
Ui_CollectionViewContainer *ui_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONVIEWCONTAINER_H
|
||||
|
||||
47
src/collection/collectionviewcontainer.ui
Normal file
47
src/collection/collectionviewcontainer.ui
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CollectionViewContainer</class>
|
||||
<widget class="QWidget" name="CollectionViewContainer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="CollectionFilterWidget" name="filter" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="CollectionView" name="view" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>CollectionFilterWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>collection/collectionfilterwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>CollectionView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>collection/collectionview.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
802
src/collection/collectionwatcher.cpp
Normal file
802
src/collection/collectionwatcher.cpp
Normal file
@@ -0,0 +1,802 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <fileref.h>
|
||||
#include <tag.h>
|
||||
|
||||
#include "collectionwatcher.h"
|
||||
|
||||
#include "collectionbackend.h"
|
||||
#include "core/filesystemwatcherinterface.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/utilities.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDirIterator>
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
||||
// This is defined by one of the windows headers that is included by taglib.
|
||||
#ifdef RemoveDirectory
|
||||
#undef RemoveDirectory
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
static const char *kNoMediaFile = ".nomedia";
|
||||
static const char *kNoMusicFile = ".nomusic";
|
||||
}
|
||||
|
||||
QStringList CollectionWatcher::sValidImages;
|
||||
|
||||
CollectionWatcher::CollectionWatcher(QObject *parent)
|
||||
: QObject(parent),
|
||||
backend_(nullptr),
|
||||
task_manager_(nullptr),
|
||||
fs_watcher_(FileSystemWatcherInterface::Create(this)),
|
||||
stop_requested_(false),
|
||||
scan_on_startup_(true),
|
||||
monitor_(true),
|
||||
rescan_timer_(new QTimer(this)),
|
||||
rescan_paused_(false),
|
||||
total_watches_(0),
|
||||
cue_parser_(new CueParser(backend_, this)) {
|
||||
Utilities::SetThreadIOPriority(Utilities::IOPRIO_CLASS_IDLE);
|
||||
|
||||
rescan_timer_->setInterval(1000);
|
||||
rescan_timer_->setSingleShot(true);
|
||||
|
||||
if (sValidImages.isEmpty()) {
|
||||
sValidImages << "jpg" << "png" << "gif" << "jpeg";
|
||||
}
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
|
||||
}
|
||||
|
||||
CollectionWatcher::ScanTransaction::ScanTransaction(CollectionWatcher *watcher, int dir, bool incremental, bool ignores_mtime)
|
||||
: progress_(0),
|
||||
progress_max_(0),
|
||||
dir_(dir),
|
||||
incremental_(incremental),
|
||||
ignores_mtime_(ignores_mtime),
|
||||
watcher_(watcher),
|
||||
cached_songs_dirty_(true),
|
||||
known_subdirs_dirty_(true) {
|
||||
|
||||
QString description;
|
||||
|
||||
if (watcher_->device_name_.isEmpty())
|
||||
description = tr("Updating collection");
|
||||
else
|
||||
description = tr("Updating %1").arg(watcher_->device_name_);
|
||||
|
||||
task_id_ = watcher_->task_manager_->StartTask(description);
|
||||
emit watcher_->ScanStarted(task_id_);
|
||||
|
||||
}
|
||||
|
||||
CollectionWatcher::ScanTransaction::~ScanTransaction() {
|
||||
|
||||
// If we're stopping then don't commit the transaction
|
||||
if (watcher_->stop_requested_) return;
|
||||
|
||||
if (!new_songs.isEmpty()) emit watcher_->NewOrUpdatedSongs(new_songs);
|
||||
|
||||
if (!touched_songs.isEmpty()) emit watcher_->SongsMTimeUpdated(touched_songs);
|
||||
|
||||
if (!deleted_songs.isEmpty()) emit watcher_->SongsDeleted(deleted_songs);
|
||||
|
||||
if (!readded_songs.isEmpty()) emit watcher_->SongsReadded(readded_songs);
|
||||
|
||||
if (!new_subdirs.isEmpty()) emit watcher_->SubdirsDiscovered(new_subdirs);
|
||||
|
||||
if (!touched_subdirs.isEmpty())
|
||||
emit watcher_->SubdirsMTimeUpdated(touched_subdirs);
|
||||
|
||||
watcher_->task_manager_->SetTaskFinished(task_id_);
|
||||
|
||||
if (watcher_->monitor_) {
|
||||
// Watch the new subdirectories
|
||||
for (const Subdirectory& subdir : new_subdirs) {
|
||||
watcher_->AddWatch(watcher_->watched_dirs_[dir_], subdir.path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::AddToProgress(int n) {
|
||||
|
||||
progress_ += n;
|
||||
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::AddToProgressMax(int n) {
|
||||
|
||||
progress_max_ += n;
|
||||
watcher_->task_manager_->SetTaskProgress(task_id_, progress_, progress_max_);
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::ScanTransaction::FindSongsInSubdirectory(const QString &path) {
|
||||
|
||||
if (cached_songs_dirty_) {
|
||||
cached_songs_ = watcher_->backend_->FindSongsInDirectory(dir_);
|
||||
cached_songs_dirty_ = false;
|
||||
}
|
||||
|
||||
// TODO: Make this faster
|
||||
SongList ret;
|
||||
for (const Song &song : cached_songs_) {
|
||||
if (song.url().toLocalFile().section('/', 0, -2) == path) ret << song;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanTransaction::SetKnownSubdirs(const SubdirectoryList &subdirs) {
|
||||
|
||||
known_subdirs_ = subdirs;
|
||||
known_subdirs_dirty_ = false;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionWatcher::ScanTransaction::HasSeenSubdir(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
|
||||
for (const Subdirectory &subdir : known_subdirs_) {
|
||||
if (subdir.path == path && subdir.mtime != 0) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetImmediateSubdirs(const QString &path) {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
|
||||
SubdirectoryList ret;
|
||||
for (const Subdirectory &subdir : known_subdirs_) {
|
||||
if (subdir.path.left(subdir.path.lastIndexOf(QDir::separator())) == path &&
|
||||
subdir.mtime != 0) {
|
||||
ret << subdir;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
SubdirectoryList CollectionWatcher::ScanTransaction::GetAllSubdirs() {
|
||||
|
||||
if (known_subdirs_dirty_)
|
||||
SetKnownSubdirs(watcher_->backend_->SubdirsInDirectory(dir_));
|
||||
return known_subdirs_;
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddDirectory(const Directory &dir, const SubdirectoryList &subdirs) {
|
||||
|
||||
watched_dirs_[dir.id] = dir;
|
||||
|
||||
if (subdirs.isEmpty()) {
|
||||
// This is a new directory that we've never seen before. Scan it fully.
|
||||
ScanTransaction transaction(this, dir.id, false);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(1);
|
||||
ScanSubdirectory(dir.path, Subdirectory(), &transaction);
|
||||
}
|
||||
else {
|
||||
// We can do an incremental scan - looking at the mtimes of each
|
||||
// subdirectory and only rescan if the directory has changed.
|
||||
ScanTransaction transaction(this, dir.id, true);
|
||||
transaction.SetKnownSubdirs(subdirs);
|
||||
transaction.AddToProgressMax(subdirs.count());
|
||||
for (const Subdirectory& subdir : subdirs) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
if (scan_on_startup_) ScanSubdirectory(subdir.path, subdir, &transaction);
|
||||
|
||||
if (monitor_) AddWatch(dir, subdir.path);
|
||||
}
|
||||
}
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ScanSubdirectory(const QString &path, const Subdirectory &subdir, ScanTransaction *t, bool force_noincremental) {
|
||||
|
||||
QFileInfo path_info(path);
|
||||
QDir path_dir(path);
|
||||
|
||||
// Do not scan symlinked dirs that are already in collection
|
||||
if (path_info.isSymLink()) {
|
||||
QString real_path = path_info.symLinkTarget();
|
||||
for (const Directory& dir : watched_dirs_) {
|
||||
if (real_path.startsWith(dir.path)) {
|
||||
t->AddToProgress(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not scan directories containing a .nomedia or .nomusic file
|
||||
if (path_dir.exists(kNoMediaFile) || path_dir.exists(kNoMusicFile)) {
|
||||
t->AddToProgress(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!t->ignores_mtime() && !force_noincremental && t->is_incremental() && subdir.mtime == path_info.lastModified().toTime_t()) {
|
||||
// The directory hasn't changed since last time
|
||||
t->AddToProgress(1);
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, QStringList> album_art;
|
||||
QStringList files_on_disk;
|
||||
SubdirectoryList my_new_subdirs;
|
||||
|
||||
// If a directory is moved then only its parent gets a changed notification,
|
||||
// so we need to look and see if any of our children don't exist any more.
|
||||
// If one has been removed, "rescan" it to get the deleted songs
|
||||
SubdirectoryList previous_subdirs = t->GetImmediateSubdirs(path);
|
||||
for (const Subdirectory& subdir : previous_subdirs) {
|
||||
if (!QFile::exists(subdir.path) && subdir.path != path) {
|
||||
t->AddToProgressMax(1);
|
||||
ScanSubdirectory(subdir.path, subdir, t, true);
|
||||
}
|
||||
}
|
||||
|
||||
// First we "quickly" get a list of the files in the directory that we think might be music. While we're here, we also look for new subdirectories and possible album artwork.
|
||||
QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot);
|
||||
while (it.hasNext()) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
QString child(it.next());
|
||||
QFileInfo child_info(child);
|
||||
|
||||
if (child_info.isDir()) {
|
||||
if (!child_info.isHidden() && !t->HasSeenSubdir(child)) {
|
||||
// We haven't seen this subdirectory before - add it to a list and
|
||||
// later we'll tell the backend about it and scan it.
|
||||
Subdirectory new_subdir;
|
||||
new_subdir.directory_id = -1;
|
||||
new_subdir.path = child;
|
||||
new_subdir.mtime = child_info.lastModified().toTime_t();
|
||||
my_new_subdirs << new_subdir;
|
||||
}
|
||||
}
|
||||
else {
|
||||
QString ext_part(ExtensionPart(child));
|
||||
QString dir_part(DirectoryPart(child));
|
||||
|
||||
if (sValidImages.contains(ext_part))
|
||||
album_art[dir_part] << child;
|
||||
else if (!child_info.isHidden())
|
||||
files_on_disk << child;
|
||||
}
|
||||
}
|
||||
|
||||
if (stop_requested_) return;
|
||||
|
||||
// Ask the database for a list of files in this directory
|
||||
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
||||
|
||||
QSet<QString> cues_processed;
|
||||
|
||||
// Now compare the list from the database with the list of files on disk
|
||||
for (const QString& file : files_on_disk) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
// associated cue
|
||||
QString matching_cue = NoExtensionPart(file) + ".cue";
|
||||
|
||||
Song matching_song;
|
||||
if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
||||
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
||||
|
||||
// The song is in the database and still on disk.
|
||||
// Check the mtime to see if it's been changed since it was added.
|
||||
QFileInfo file_info(file);
|
||||
|
||||
if (!file_info.exists()) {
|
||||
// Partially fixes race condition - if file was removed between being
|
||||
// added to the list and now.
|
||||
files_on_disk.removeAll(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
// cue sheet's path from collection (if any)
|
||||
QString song_cue = matching_song.cue_path();
|
||||
uint song_cue_mtime = GetMtimeForCue(song_cue);
|
||||
|
||||
bool cue_deleted = song_cue_mtime == 0 && matching_song.has_cue();
|
||||
bool cue_added = matching_cue_mtime != 0 && !matching_song.has_cue();
|
||||
|
||||
// watch out for cue songs which have their mtime equal to
|
||||
// qMax(media_file_mtime, cue_sheet_mtime)
|
||||
bool changed = (matching_song.mtime() != qMax(file_info.lastModified().toTime_t(), song_cue_mtime)) || cue_deleted || cue_added;
|
||||
|
||||
// Also want to look to see whether the album art has changed
|
||||
QString image = ImageForSong(file, album_art);
|
||||
if ((matching_song.art_automatic().isEmpty() && !image.isEmpty()) || (!matching_song.art_automatic().isEmpty() && !matching_song.has_embedded_cover() && !QFile::exists(matching_song.art_automatic()))) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// the song's changed - reread the metadata from file
|
||||
if (t->ignores_mtime() || changed) {
|
||||
qLog(Debug) << file << "changed";
|
||||
|
||||
// if cue associated...
|
||||
if (!cue_deleted && (matching_song.has_cue() || cue_added)) {
|
||||
UpdateCueAssociatedSongs(file, path, matching_cue, image, t);
|
||||
// if no cue or it's about to lose it...
|
||||
}
|
||||
else {
|
||||
UpdateNonCueAssociatedSong(file, matching_song, image, cue_deleted, t);
|
||||
}
|
||||
}
|
||||
|
||||
// nothing has changed - mark the song available without re-scanning
|
||||
if (matching_song.is_unavailable()) t->readded_songs << matching_song;
|
||||
|
||||
} else {
|
||||
// The song is on disk but not in the DB
|
||||
SongList song_list = ScanNewFile(file, path, matching_cue, &cues_processed);
|
||||
|
||||
if (song_list.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qLog(Debug) << file << "created";
|
||||
// choose an image for the song(s)
|
||||
QString image = ImageForSong(file, album_art);
|
||||
|
||||
for (Song song : song_list) {
|
||||
song.set_directory_id(t->dir());
|
||||
if (song.art_automatic().isEmpty()) song.set_art_automatic(image);
|
||||
|
||||
t->new_songs << song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for deleted songs
|
||||
for (const Song& song : songs_in_db) {
|
||||
if (!song.is_unavailable() && !files_on_disk.contains(song.url().toLocalFile())) {
|
||||
qLog(Debug) << "Song deleted from disk:" << song.url().toLocalFile();
|
||||
t->deleted_songs << song;
|
||||
}
|
||||
}
|
||||
|
||||
// Add this subdir to the new or touched list
|
||||
Subdirectory updated_subdir;
|
||||
updated_subdir.directory_id = t->dir();
|
||||
updated_subdir.mtime = path_info.exists() ? path_info.lastModified().toTime_t() : 0;
|
||||
updated_subdir.path = path;
|
||||
|
||||
if (subdir.directory_id == -1)
|
||||
t->new_subdirs << updated_subdir;
|
||||
else
|
||||
t->touched_subdirs << updated_subdir;
|
||||
|
||||
t->AddToProgress(1);
|
||||
|
||||
// Recurse into the new subdirs that we found
|
||||
t->AddToProgressMax(my_new_subdirs.count());
|
||||
for (const Subdirectory& my_new_subdir : my_new_subdirs) {
|
||||
if (stop_requested_) return;
|
||||
ScanSubdirectory(my_new_subdir.path, my_new_subdir, t, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QString &image, ScanTransaction *t) {
|
||||
|
||||
QFile cue(matching_cue);
|
||||
cue.open(QIODevice::ReadOnly);
|
||||
|
||||
SongList old_sections = backend_->GetSongsByUrl(QUrl::fromLocalFile(file));
|
||||
|
||||
QHash<quint64, Song> sections_map;
|
||||
for (const Song& song : old_sections) {
|
||||
sections_map[song.beginning_nanosec()] = song;
|
||||
}
|
||||
|
||||
QSet<int> used_ids;
|
||||
|
||||
// update every song that's in the cue and collection
|
||||
for (Song cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
cue_song.set_directory_id(t->dir());
|
||||
|
||||
Song matching = sections_map[cue_song.beginning_nanosec()];
|
||||
// a new section
|
||||
if (!matching.is_valid()) {
|
||||
t->new_songs << cue_song;
|
||||
// changed section
|
||||
} else {
|
||||
PreserveUserSetData(file, image, matching, &cue_song, t);
|
||||
used_ids.insert(matching.id());
|
||||
}
|
||||
}
|
||||
|
||||
// sections that are now missing
|
||||
for (const Song &matching : old_sections) {
|
||||
if (!used_ids.contains(matching.id())) {
|
||||
t->deleted_songs << matching;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QString &image, bool cue_deleted, ScanTransaction *t) {
|
||||
|
||||
// if a cue got deleted, we turn it's first section into the new
|
||||
// 'raw' (cueless) song and we just remove the rest of the sections
|
||||
// from the collection
|
||||
if (cue_deleted) {
|
||||
for (const Song &song :
|
||||
backend_->GetSongsByUrl(QUrl::fromLocalFile(file))) {
|
||||
if (!song.IsMetadataEqual(matching_song)) {
|
||||
t->deleted_songs << song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Song song_on_disk;
|
||||
song_on_disk.set_directory_id(t->dir());
|
||||
TagReaderClient::Instance()->ReadFileBlocking(file, &song_on_disk);
|
||||
|
||||
if (song_on_disk.is_valid()) {
|
||||
PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SongList CollectionWatcher::ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet<QString> *cues_processed) {
|
||||
|
||||
SongList song_list;
|
||||
|
||||
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
||||
// if it's a cue - create virtual tracks
|
||||
if (matching_cue_mtime) {
|
||||
// don't process the same cue many times
|
||||
if (cues_processed->contains(matching_cue)) return song_list;
|
||||
|
||||
QFile cue(matching_cue);
|
||||
cue.open(QIODevice::ReadOnly);
|
||||
|
||||
// Ignore FILEs pointing to other media files. Also, watch out for incorrect
|
||||
// media files. Playlist parser for CUEs considers every entry in sheet
|
||||
// valid and we don't want invalid media getting into collection!
|
||||
QString file_nfd = file.normalized(QString::NormalizationForm_D);
|
||||
for (const Song& cue_song : cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
if (cue_song.url().toLocalFile().normalized(QString::NormalizationForm_D) == file_nfd) {
|
||||
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
|
||||
song_list << cue_song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!song_list.isEmpty()) {
|
||||
*cues_processed << matching_cue;
|
||||
}
|
||||
|
||||
// it's a normal media file
|
||||
}
|
||||
else {
|
||||
Song song;
|
||||
TagReaderClient::Instance()->ReadFileBlocking(file, &song);
|
||||
|
||||
if (song.is_valid()) {
|
||||
song_list << song;
|
||||
}
|
||||
}
|
||||
|
||||
return song_list;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::PreserveUserSetData(const QString &file, const QString &image, const Song &matching_song, Song *out, ScanTransaction *t) {
|
||||
|
||||
out->set_id(matching_song.id());
|
||||
|
||||
// Previous versions of Clementine incorrectly overwrote this and
|
||||
// stored it in the DB, so we can't rely on matching_song to
|
||||
// know if it has embedded artwork or not, but we can check here.
|
||||
if (!out->has_embedded_cover()) out->set_art_automatic(image);
|
||||
|
||||
out->MergeUserSetData(matching_song);
|
||||
|
||||
// The song was deleted from the database (e.g. due to an unmounted
|
||||
// filesystem), but has been restored.
|
||||
if (matching_song.is_unavailable()) {
|
||||
qLog(Debug) << file << " unavailable song restored";
|
||||
|
||||
t->new_songs << *out;
|
||||
}
|
||||
else if (!matching_song.IsMetadataEqual(*out)) {
|
||||
qLog(Debug) << file << "metadata changed";
|
||||
|
||||
// Update the song in the DB
|
||||
t->new_songs << *out;
|
||||
}
|
||||
else {
|
||||
// Only the mtime's changed
|
||||
t->touched_songs << *out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint CollectionWatcher::GetMtimeForCue(const QString &cue_path) {
|
||||
|
||||
// slight optimisation
|
||||
if (cue_path.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QFileInfo file_info(cue_path);
|
||||
if (!file_info.exists()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const QDateTime cue_last_modified = file_info.lastModified();
|
||||
|
||||
return cue_last_modified.isValid() ? cue_last_modified.toTime_t() : 0;
|
||||
}
|
||||
|
||||
void CollectionWatcher::AddWatch(const Directory &dir, const QString &path) {
|
||||
|
||||
if (!QFile::exists(path)) return;
|
||||
|
||||
connect(fs_watcher_, SIGNAL(PathChanged(const QString&)), this, SLOT(DirectoryChanged(const QString&)), Qt::UniqueConnection);
|
||||
fs_watcher_->AddPath(path);
|
||||
subdir_mapping_[path] = dir;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RemoveDirectory(const Directory& dir) {
|
||||
|
||||
rescan_queue_.remove(dir.id);
|
||||
watched_dirs_.remove(dir.id);
|
||||
|
||||
// Stop watching the directory's subdirectories
|
||||
for (const QString& subdir_path : subdir_mapping_.keys(dir)) {
|
||||
fs_watcher_->RemovePath(subdir_path);
|
||||
subdir_mapping_.remove(subdir_path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CollectionWatcher::FindSongByPath(const SongList &list, const QString &path, Song *out) {
|
||||
|
||||
// TODO: Make this faster
|
||||
for (const Song &song : list) {
|
||||
if (song.url().toLocalFile() == path) {
|
||||
*out = song;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::DirectoryChanged(const QString &subdir) {
|
||||
|
||||
// Find what dir it was in
|
||||
QHash<QString, Directory>::const_iterator it = subdir_mapping_.constFind(subdir);
|
||||
if (it == subdir_mapping_.constEnd()) {
|
||||
return;
|
||||
}
|
||||
Directory dir = *it;
|
||||
|
||||
qLog(Debug) << "Subdir" << subdir << "changed under directory" << dir.path << "id" << dir.id;
|
||||
|
||||
// Queue the subdir for rescanning
|
||||
if (!rescan_queue_[dir.id].contains(subdir)) rescan_queue_[dir.id] << subdir;
|
||||
|
||||
if (!rescan_paused_) rescan_timer_->start();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::RescanPathsNow() {
|
||||
|
||||
for (int dir : rescan_queue_.keys()) {
|
||||
if (stop_requested_) return;
|
||||
ScanTransaction transaction(this, dir, false);
|
||||
transaction.AddToProgressMax(rescan_queue_[dir].count());
|
||||
|
||||
for (const QString &path : rescan_queue_[dir]) {
|
||||
if (stop_requested_) return;
|
||||
Subdirectory subdir;
|
||||
subdir.directory_id = dir;
|
||||
subdir.mtime = 0;
|
||||
subdir.path = path;
|
||||
ScanSubdirectory(path, subdir, &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
rescan_queue_.clear();
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
|
||||
QString CollectionWatcher::PickBestImage(const QStringList &images) {
|
||||
|
||||
// This is used when there is more than one image in a directory.
|
||||
// Pick the biggest image that matches the most important filter
|
||||
|
||||
QStringList filtered;
|
||||
|
||||
for (const QString &filter_text : best_image_filters_) {
|
||||
// the images in the images list are represented by a full path,
|
||||
// so we need to isolate just the filename
|
||||
for (const QString& image : images) {
|
||||
QFileInfo file_info(image);
|
||||
QString filename(file_info.fileName());
|
||||
if (filename.contains(filter_text, Qt::CaseInsensitive))
|
||||
filtered << image;
|
||||
}
|
||||
|
||||
/* We assume the filters are give in the order best to worst, so
|
||||
if we've got a result, we go with it. Otherwise we might
|
||||
start capturing more generic rules */
|
||||
if (!filtered.isEmpty()) break;
|
||||
}
|
||||
|
||||
if (filtered.isEmpty()) {
|
||||
// the filter was too restrictive, just use the original list
|
||||
filtered = images;
|
||||
}
|
||||
|
||||
int biggest_size = 0;
|
||||
QString biggest_path;
|
||||
|
||||
for (const QString& path : filtered) {
|
||||
QImage image(path);
|
||||
if (image.isNull()) continue;
|
||||
|
||||
int size = image.width() * image.height();
|
||||
if (size > biggest_size) {
|
||||
biggest_size = size;
|
||||
biggest_path = path;
|
||||
}
|
||||
}
|
||||
|
||||
return biggest_path;
|
||||
|
||||
}
|
||||
|
||||
QString CollectionWatcher::ImageForSong(const QString &path, QMap<QString, QStringList> &album_art) {
|
||||
|
||||
QString dir(DirectoryPart(path));
|
||||
|
||||
if (album_art.contains(dir)) {
|
||||
if (album_art[dir].count() == 1)
|
||||
return album_art[dir][0];
|
||||
else {
|
||||
QString best_image = PickBestImage(album_art[dir]);
|
||||
album_art[dir] = QStringList() << best_image;
|
||||
return best_image;
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ReloadSettingsAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "ReloadSettings", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::ReloadSettings() {
|
||||
|
||||
const bool was_monitoring_before = monitor_;
|
||||
QSettings s;
|
||||
|
||||
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
scan_on_startup_ = s.value("startup_scan", true).toBool();
|
||||
monitor_ = s.value("monitor", true).toBool();
|
||||
|
||||
best_image_filters_.clear();
|
||||
QStringList filters = s.value("cover_art_patterns", QStringList() << "front" << "cover").toStringList();
|
||||
for (const QString& filter : filters) {
|
||||
QString s = filter.trimmed();
|
||||
if (!s.isEmpty()) best_image_filters_ << s;
|
||||
}
|
||||
|
||||
if (!monitor_ && was_monitoring_before) {
|
||||
fs_watcher_->Clear();
|
||||
}
|
||||
else if (monitor_ && !was_monitoring_before) {
|
||||
// Add all directories to all QFileSystemWatchers again
|
||||
for (const Directory& dir : watched_dirs_.values()) {
|
||||
SubdirectoryList subdirs = backend_->SubdirsInDirectory(dir.id);
|
||||
for (const Subdirectory& subdir : subdirs) {
|
||||
AddWatch(dir, subdir.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::SetRescanPausedAsync(bool pause) {
|
||||
|
||||
QMetaObject::invokeMethod(this, "SetRescanPaused", Qt::QueuedConnection, Q_ARG(bool, pause));
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::SetRescanPaused(bool pause) {
|
||||
|
||||
rescan_paused_ = pause;
|
||||
if (!rescan_paused_ && !rescan_queue_.isEmpty()) RescanPathsNow();
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::IncrementalScanAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "IncrementalScanNow", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::FullScanAsync() {
|
||||
|
||||
QMetaObject::invokeMethod(this, "FullScanNow", Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
void CollectionWatcher::IncrementalScanNow() { PerformScan(true, false); }
|
||||
|
||||
void CollectionWatcher::FullScanNow() { PerformScan(false, true); }
|
||||
|
||||
void CollectionWatcher::PerformScan(bool incremental, bool ignore_mtimes) {
|
||||
|
||||
for (const Directory & dir : watched_dirs_.values()) {
|
||||
ScanTransaction transaction(this, dir.id, incremental, ignore_mtimes);
|
||||
SubdirectoryList subdirs(transaction.GetAllSubdirs());
|
||||
transaction.AddToProgressMax(subdirs.count());
|
||||
|
||||
for (const Subdirectory & subdir : subdirs) {
|
||||
if (stop_requested_) return;
|
||||
|
||||
ScanSubdirectory(subdir.path, subdir, &transaction);
|
||||
}
|
||||
}
|
||||
|
||||
emit CompilationsNeedUpdating();
|
||||
|
||||
}
|
||||
213
src/collection/collectionwatcher.h
Normal file
213
src/collection/collectionwatcher.h
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COLLECTIONWATCHER_H
|
||||
#define COLLECTIONWATCHER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "directory.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class QFileSystemWatcher;
|
||||
class QTimer;
|
||||
|
||||
class CueParser;
|
||||
class FileSystemWatcherInterface;
|
||||
class CollectionBackend;
|
||||
class TaskManager;
|
||||
|
||||
class CollectionWatcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CollectionWatcher(QObject *parent = nullptr);
|
||||
|
||||
void set_backend(CollectionBackend *backend) { backend_ = backend; }
|
||||
void set_task_manager(TaskManager *task_manager) { task_manager_ = task_manager; }
|
||||
void set_device_name(const QString& device_name) { device_name_ = device_name; }
|
||||
|
||||
void IncrementalScanAsync();
|
||||
void FullScanAsync();
|
||||
void SetRescanPausedAsync(bool pause);
|
||||
void ReloadSettingsAsync();
|
||||
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
signals:
|
||||
void NewOrUpdatedSongs(const SongList &songs);
|
||||
void SongsMTimeUpdated(const SongList &songs);
|
||||
void SongsDeleted(const SongList &songs);
|
||||
void SongsReadded(const SongList &songs, bool unavailable = false);
|
||||
void SubdirsDiscovered(const SubdirectoryList &subdirs);
|
||||
void SubdirsMTimeUpdated(const SubdirectoryList &subdirs);
|
||||
void CompilationsNeedUpdating();
|
||||
|
||||
void ScanStarted(int task_id);
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
void AddDirectory(const Directory &dir, const SubdirectoryList &subdirs);
|
||||
void RemoveDirectory(const Directory &dir);
|
||||
void SetRescanPaused(bool pause);
|
||||
|
||||
private:
|
||||
// This class encapsulates a full or partial scan of a directory.
|
||||
// Each directory has one or more subdirectories, and any number of
|
||||
// subdirectories can be scanned during one transaction. ScanSubdirectory()
|
||||
// adds its results to the members of this transaction class, and they are
|
||||
// "committed" through calls to the CollectionBackend in the transaction's dtor.
|
||||
// The transaction also caches the list of songs in this directory according
|
||||
// to the collection. Multiple calls to FindSongsInSubdirectory during one
|
||||
// transaction will only result in one call to
|
||||
// CollectionBackend::FindSongsInDirectory.
|
||||
class ScanTransaction {
|
||||
public:
|
||||
ScanTransaction(CollectionWatcher *watcher, int dir, bool incremental, bool ignores_mtime = false);
|
||||
~ScanTransaction();
|
||||
|
||||
SongList FindSongsInSubdirectory(const QString &path);
|
||||
bool HasSeenSubdir(const QString &path);
|
||||
void SetKnownSubdirs(const SubdirectoryList &subdirs);
|
||||
SubdirectoryList GetImmediateSubdirs(const QString &path);
|
||||
SubdirectoryList GetAllSubdirs();
|
||||
|
||||
void AddToProgress(int n = 1);
|
||||
void AddToProgressMax(int n);
|
||||
|
||||
int dir() const { return dir_; }
|
||||
bool is_incremental() const { return incremental_; }
|
||||
bool ignores_mtime() const { return ignores_mtime_; }
|
||||
|
||||
SongList deleted_songs;
|
||||
SongList readded_songs;
|
||||
SongList new_songs;
|
||||
SongList touched_songs;
|
||||
SubdirectoryList new_subdirs;
|
||||
SubdirectoryList touched_subdirs;
|
||||
|
||||
private:
|
||||
ScanTransaction(const ScanTransaction&) {}
|
||||
ScanTransaction& operator=(const ScanTransaction&) { return *this; }
|
||||
|
||||
int task_id_;
|
||||
int progress_;
|
||||
int progress_max_;
|
||||
|
||||
int dir_;
|
||||
// Incremental scan enters a directory only if it has changed since the last scan.
|
||||
bool incremental_;
|
||||
// This type of scan updates every file in a folder that's
|
||||
// being scanned. Even if it detects the file hasn't changed since
|
||||
// the last scan. Also, since it's ignoring mtimes on folders too,
|
||||
// it will go as deep in the folder hierarchy as it's possible.
|
||||
bool ignores_mtime_;
|
||||
|
||||
CollectionWatcher *watcher_;
|
||||
|
||||
SongList cached_songs_;
|
||||
bool cached_songs_dirty_;
|
||||
|
||||
SubdirectoryList known_subdirs_;
|
||||
bool known_subdirs_dirty_;
|
||||
};
|
||||
|
||||
private slots:
|
||||
void DirectoryChanged(const QString &path);
|
||||
void IncrementalScanNow();
|
||||
void FullScanNow();
|
||||
void RescanPathsNow();
|
||||
void ScanSubdirectory(const QString &path, const Subdirectory &subdir, ScanTransaction *t, bool force_noincremental = false);
|
||||
|
||||
private:
|
||||
static bool FindSongByPath(const SongList &list, const QString &path, Song *out);
|
||||
inline static QString NoExtensionPart(const QString &fileName);
|
||||
inline static QString ExtensionPart(const QString &fileName);
|
||||
inline static QString DirectoryPart(const QString &fileName);
|
||||
QString PickBestImage(const QStringList &images);
|
||||
QString ImageForSong(const QString &path, QMap<QString, QStringList> &album_art);
|
||||
void AddWatch(const Directory &dir, const QString &path);
|
||||
uint GetMtimeForCue(const QString &cue_path);
|
||||
void PerformScan(bool incremental, bool ignore_mtimes);
|
||||
|
||||
// Updates the sections of a cue associated and altered (according to mtime)
|
||||
// media file during a scan.
|
||||
void UpdateCueAssociatedSongs(const QString &file, const QString &path, const QString &matching_cue, const QString &image, ScanTransaction *t);
|
||||
// Updates a single non-cue associated and altered (according to mtime) song
|
||||
// during a scan.
|
||||
void UpdateNonCueAssociatedSong(const QString &file, const Song &matching_song, const QString &image, bool cue_deleted, ScanTransaction *t);
|
||||
// Updates a new song with some metadata taken from it's equivalent old
|
||||
// song (for example rating and score).
|
||||
void PreserveUserSetData(const QString &file, const QString &image, const Song &matching_song, Song *out, ScanTransaction *t);
|
||||
// Scans a single media file that's present on the disk but not yet in the collection.
|
||||
// It may result in a multiple files added to the collection when the media file
|
||||
// has many sections (like a CUE related media file).
|
||||
SongList ScanNewFile(const QString &file, const QString &path, const QString &matching_cue, QSet<QString> *cues_processed);
|
||||
|
||||
private:
|
||||
CollectionBackend *backend_;
|
||||
TaskManager *task_manager_;
|
||||
QString device_name_;
|
||||
|
||||
FileSystemWatcherInterface *fs_watcher_;
|
||||
QHash<QString, Directory> subdir_mapping_;
|
||||
|
||||
/* A list of words use to try to identify the (likely) best image
|
||||
* found in an directory to use as cover artwork.
|
||||
* e.g. using ["front", "cover"] would identify front.jpg and
|
||||
* exclude back.jpg.
|
||||
*/
|
||||
QStringList best_image_filters_;
|
||||
|
||||
bool stop_requested_;
|
||||
bool scan_on_startup_;
|
||||
bool monitor_;
|
||||
|
||||
QMap<int, Directory> watched_dirs_;
|
||||
QTimer *rescan_timer_;
|
||||
QMap<int, QStringList> rescan_queue_; // dir id -> list of subdirs to be scanned
|
||||
bool rescan_paused_;
|
||||
|
||||
int total_watches_;
|
||||
|
||||
CueParser *cue_parser_;
|
||||
|
||||
static QStringList sValidImages;
|
||||
};
|
||||
|
||||
inline QString CollectionWatcher::NoExtensionPart(const QString& fileName) {
|
||||
return fileName.contains('.') ? fileName.section('.', 0, -2) : "";
|
||||
}
|
||||
// Thanks Amarok
|
||||
inline QString CollectionWatcher::ExtensionPart(const QString& fileName) {
|
||||
return fileName.contains( '.' ) ? fileName.mid( fileName.lastIndexOf('.') + 1 ).toLower() : "";
|
||||
}
|
||||
inline QString CollectionWatcher::DirectoryPart(const QString& fileName) {
|
||||
return fileName.section('/', 0, -2);
|
||||
}
|
||||
|
||||
#endif // COLLECTIONWATCHER_H
|
||||
|
||||
61
src/collection/directory.h
Normal file
61
src/collection/directory.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 DIRECTORY_H
|
||||
#define DIRECTORY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QMetaType>
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
struct Directory {
|
||||
Directory() : id(-1) {}
|
||||
|
||||
bool operator ==(const Directory& other) const {
|
||||
return path == other.path && id == other.id;
|
||||
}
|
||||
|
||||
QString path;
|
||||
int id;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Directory)
|
||||
|
||||
typedef QList<Directory> DirectoryList;
|
||||
Q_DECLARE_METATYPE(DirectoryList)
|
||||
|
||||
|
||||
struct Subdirectory {
|
||||
Subdirectory() : directory_id(-1), mtime(0) {}
|
||||
|
||||
int directory_id;
|
||||
QString path;
|
||||
uint mtime;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Subdirectory)
|
||||
|
||||
typedef QList<Subdirectory> SubdirectoryList;
|
||||
Q_DECLARE_METATYPE(SubdirectoryList)
|
||||
|
||||
#endif // DIRECTORY_H
|
||||
|
||||
121
src/collection/groupbydialog.cpp
Normal file
121
src/collection/groupbydialog.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <functional>
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
#include "groupbydialog.h"
|
||||
#include "ui_groupbydialog.h"
|
||||
|
||||
// boost::multi_index still relies on these being in the global namespace.
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
|
||||
#include <boost/multi_index_container.hpp>
|
||||
#include <boost/multi_index/member.hpp>
|
||||
#include <boost/multi_index/ordered_index.hpp>
|
||||
|
||||
using boost::multi_index_container;
|
||||
using boost::multi_index::indexed_by;
|
||||
using boost::multi_index::ordered_unique;
|
||||
using boost::multi_index::tag;
|
||||
using boost::multi_index::member;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Mapping {
|
||||
Mapping(CollectionModel::GroupBy g, int i) : group_by(g), combo_box_index(i) {}
|
||||
|
||||
CollectionModel::GroupBy group_by;
|
||||
int combo_box_index;
|
||||
};
|
||||
|
||||
struct tag_index {};
|
||||
struct tag_group_by {};
|
||||
|
||||
} // namespace
|
||||
|
||||
class GroupByDialogPrivate {
|
||||
private:
|
||||
typedef multi_index_container<
|
||||
Mapping,
|
||||
indexed_by<
|
||||
ordered_unique<tag<tag_index>,
|
||||
member<Mapping, int, &Mapping::combo_box_index> >,
|
||||
ordered_unique<tag<tag_group_by>,
|
||||
member<Mapping, CollectionModel::GroupBy,
|
||||
&Mapping::group_by> > > > MappingContainer;
|
||||
|
||||
public:
|
||||
MappingContainer mapping_;
|
||||
};
|
||||
|
||||
GroupByDialog::GroupByDialog(QWidget* parent) : QDialog(parent), ui_(new Ui_GroupByDialog), p_(new GroupByDialogPrivate) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
Reset();
|
||||
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_None, 0));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Album, 1));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Artist, 2));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_AlbumArtist, 3));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Composer, 4));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_FileType, 5));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Genre, 6));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Year, 7));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYear, 8));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_YearAlbum, 9));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_OriginalYearAlbum, 10));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Bitrate, 11));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Disc, 12));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Performer, 13));
|
||||
p_->mapping_.insert(Mapping(CollectionModel::GroupBy_Grouping, 14));
|
||||
|
||||
connect(ui_->button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()), SLOT(Reset()));
|
||||
|
||||
resize(sizeHint());
|
||||
}
|
||||
|
||||
GroupByDialog::~GroupByDialog() {}
|
||||
|
||||
void GroupByDialog::Reset() {
|
||||
ui_->first->setCurrentIndex(2); // Artist
|
||||
ui_->second->setCurrentIndex(1); // Album
|
||||
ui_->third->setCurrentIndex(0); // None
|
||||
}
|
||||
|
||||
void GroupByDialog::accept() {
|
||||
emit Accepted(CollectionModel::Grouping(
|
||||
p_->mapping_.get<tag_index>().find(ui_->first->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->second->currentIndex())->group_by,
|
||||
p_->mapping_.get<tag_index>().find(ui_->third->currentIndex())->group_by)
|
||||
);
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void GroupByDialog::CollectionGroupingChanged(const CollectionModel::Grouping &g) {
|
||||
ui_->first->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[0])->combo_box_index);
|
||||
ui_->second->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[1])->combo_box_index);
|
||||
ui_->third->setCurrentIndex(p_->mapping_.get<tag_group_by>().find(g[2])->combo_box_index);
|
||||
}
|
||||
|
||||
57
src/collection/groupbydialog.h
Normal file
57
src/collection/groupbydialog.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 GROUPBYDIALOG_H
|
||||
#define GROUPBYDIALOG_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
|
||||
class GroupByDialogPrivate;
|
||||
class Ui_GroupByDialog;
|
||||
|
||||
class GroupByDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GroupByDialog(QWidget *parent = nullptr);
|
||||
~GroupByDialog();
|
||||
|
||||
public slots:
|
||||
void CollectionGroupingChanged(const CollectionModel::Grouping &g);
|
||||
void accept();
|
||||
|
||||
signals:
|
||||
void Accepted(const CollectionModel::Grouping &g);
|
||||
|
||||
private slots:
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui_GroupByDialog> ui_;
|
||||
std::unique_ptr<GroupByDialogPrivate> p_;
|
||||
};
|
||||
|
||||
#endif // GROUPBYDIALOG_H
|
||||
366
src/collection/groupbydialog.ui
Normal file
366
src/collection/groupbydialog.ui
Normal file
@@ -0,0 +1,366 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GroupByDialog</class>
|
||||
<widget class="QDialog" name="GroupByDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>354</width>
|
||||
<height>236</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Collection advanced grouping</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>You can change the way the songs in the collection are organised.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Group Collection by...</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>First level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="first">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Second level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="second">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Third level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="third">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Album artist</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Composer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>File type</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Genre</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Original year - Album</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bitrate</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Disc</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Performer</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Grouping</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>11</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>first</tabstop>
|
||||
<tabstop>second</tabstop>
|
||||
<tabstop>third</tabstop>
|
||||
<tabstop>button_box</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>GroupByDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>GroupByDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
164
src/collection/savedgroupingmanager.cpp
Normal file
164
src/collection/savedgroupingmanager.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "core/iconloader.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "ui_savedgroupingmanager.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
#include <QList>
|
||||
#include <QSettings>
|
||||
#include <QStandardItem>
|
||||
|
||||
SavedGroupingManager::SavedGroupingManager(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_SavedGroupingManager),
|
||||
model_(new QStandardItemModel(0, 4, this)) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
model_->setHorizontalHeaderItem(0, new QStandardItem(tr("Name")));
|
||||
model_->setHorizontalHeaderItem(1, new QStandardItem(tr("First level")));
|
||||
model_->setHorizontalHeaderItem(2, new QStandardItem(tr("Second Level")));
|
||||
model_->setHorizontalHeaderItem(3, new QStandardItem(tr("Third Level")));
|
||||
ui_->list->setModel(model_);
|
||||
ui_->remove->setIcon(IconLoader::Load("edit-delete"));
|
||||
ui_->remove->setEnabled(false);
|
||||
|
||||
ui_->remove->setShortcut(QKeySequence::Delete);
|
||||
connect(ui_->list->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(UpdateButtonState()));
|
||||
|
||||
connect(ui_->remove, SIGNAL(clicked()), SLOT(Remove()));
|
||||
}
|
||||
|
||||
SavedGroupingManager::~SavedGroupingManager() {
|
||||
delete ui_;
|
||||
delete model_;
|
||||
}
|
||||
|
||||
QString SavedGroupingManager::GroupByToString(const CollectionModel::GroupBy &g) {
|
||||
switch (g) {
|
||||
case CollectionModel::GroupBy_None: {
|
||||
return tr("None");
|
||||
}
|
||||
case CollectionModel::GroupBy_Artist: {
|
||||
return tr("Artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_Album: {
|
||||
return tr("Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_YearAlbum: {
|
||||
return tr("Year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_Year: {
|
||||
return tr("Year");
|
||||
}
|
||||
case CollectionModel::GroupBy_Composer: {
|
||||
return tr("Composer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Genre: {
|
||||
return tr("Genre");
|
||||
}
|
||||
case CollectionModel::GroupBy_AlbumArtist: {
|
||||
return tr("Album artist");
|
||||
}
|
||||
case CollectionModel::GroupBy_FileType: {
|
||||
return tr("File type");
|
||||
}
|
||||
case CollectionModel::GroupBy_Performer: {
|
||||
return tr("Performer");
|
||||
}
|
||||
case CollectionModel::GroupBy_Grouping: {
|
||||
return tr("Grouping");
|
||||
}
|
||||
case CollectionModel::GroupBy_Bitrate: {
|
||||
return tr("Bitrate");
|
||||
}
|
||||
case CollectionModel::GroupBy_Disc: {
|
||||
return tr("Disc");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYearAlbum: {
|
||||
return tr("Original year - Album");
|
||||
}
|
||||
case CollectionModel::GroupBy_OriginalYear: {
|
||||
return tr("Original year");
|
||||
}
|
||||
default: { return tr("Unknown"); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateModel() {
|
||||
|
||||
model_->setRowCount(0); // don't use clear, it deletes headers
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
QStringList saved = s.childKeys();
|
||||
for (int i = 0; i < saved.size(); ++i) {
|
||||
QByteArray bytes = s.value(saved.at(i)).toByteArray();
|
||||
QDataStream ds(&bytes, QIODevice::ReadOnly);
|
||||
CollectionModel::Grouping g;
|
||||
ds >> g;
|
||||
|
||||
QList<QStandardItem*> list;
|
||||
list << new QStandardItem(saved.at(i))
|
||||
<< new QStandardItem(GroupByToString(g.first))
|
||||
<< new QStandardItem(GroupByToString(g.second))
|
||||
<< new QStandardItem(GroupByToString(g.third));
|
||||
|
||||
model_->appendRow(list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::Remove() {
|
||||
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
QSettings s;
|
||||
s.beginGroup(CollectionModel::kSavedGroupingsSettingsGroup);
|
||||
for (const QModelIndex &index :
|
||||
ui_->list->selectionModel()->selectedRows()) {
|
||||
if (index.isValid()) {
|
||||
qLog(Debug) << "Remove saved grouping: " << model_->item(index.row(), 0)->text();
|
||||
s.remove(model_->item(index.row(), 0)->text());
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateModel();
|
||||
filter_->UpdateGroupByActions();
|
||||
|
||||
}
|
||||
|
||||
void SavedGroupingManager::UpdateButtonState() {
|
||||
|
||||
if (ui_->list->selectionModel()->hasSelection()) {
|
||||
const QModelIndex current = ui_->list->selectionModel()->currentIndex();
|
||||
ui_->remove->setEnabled(current.isValid());
|
||||
}
|
||||
else {
|
||||
ui_->remove->setEnabled(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
56
src/collection/savedgroupingmanager.h
Normal file
56
src/collection/savedgroupingmanager.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2015, Nick Lanham <nick@afternight.org>
|
||||
*
|
||||
* 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 SAVEDGROUPINGMANAGER_H
|
||||
#define SAVEDGROUPINGMANAGER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
#include "collectionmodel.h"
|
||||
|
||||
class Ui_SavedGroupingManager;
|
||||
class CollectionFilterWidget;
|
||||
|
||||
class SavedGroupingManager : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SavedGroupingManager(QWidget *parent = nullptr);
|
||||
~SavedGroupingManager();
|
||||
|
||||
void UpdateModel();
|
||||
void SetFilter(CollectionFilterWidget* filter) { filter_ = filter; }
|
||||
|
||||
static QString GroupByToString(const CollectionModel::GroupBy &g);
|
||||
|
||||
private slots:
|
||||
void UpdateButtonState();
|
||||
void Remove();
|
||||
|
||||
private:
|
||||
Ui_SavedGroupingManager* ui_;
|
||||
QStandardItemModel *model_;
|
||||
CollectionFilterWidget *filter_;
|
||||
};
|
||||
|
||||
#endif // SAVEDGROUPINGMANAGER_H
|
||||
144
src/collection/savedgroupingmanager.ui
Normal file
144
src/collection/savedgroupingmanager.ui
Normal file
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SavedGroupingManager</class>
|
||||
<widget class="QDialog" name="SavedGroupingManager">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>582</width>
|
||||
<height>363</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Saved Grouping Manager</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QTreeView" name="list">
|
||||
<property name="acceptDrops">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragDrop</enum>
|
||||
</property>
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>SavedGroupingManager</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
40
src/collection/sqlrow.cpp
Normal file
40
src/collection/sqlrow.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "collectionquery.h"
|
||||
#include "sqlrow.h"
|
||||
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlRecord>
|
||||
|
||||
SqlRow::SqlRow(const QSqlQuery &query) { Init(query); }
|
||||
|
||||
SqlRow::SqlRow(const CollectionQuery &query) { Init(query); }
|
||||
|
||||
void SqlRow::Init(const QSqlQuery &query) {
|
||||
|
||||
int rows = query.record().count();
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
columns_ << query.value(i);
|
||||
}
|
||||
|
||||
}
|
||||
54
src/collection/sqlrow.h
Normal file
54
src/collection/sqlrow.h
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 SQLROW_H
|
||||
#define SQLROW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
|
||||
class QSqlQuery;
|
||||
|
||||
class CollectionQuery;
|
||||
|
||||
class SqlRow {
|
||||
|
||||
public:
|
||||
// WARNING: Implicit construction from QSqlQuery and CollectionQuery.
|
||||
SqlRow(const QSqlQuery &query);
|
||||
SqlRow(const CollectionQuery &query);
|
||||
|
||||
const QVariant& value(int i) const { return columns_[i]; }
|
||||
|
||||
QList<QVariant> columns_;
|
||||
|
||||
private:
|
||||
SqlRow();
|
||||
|
||||
void Init(const QSqlQuery &query);
|
||||
|
||||
};
|
||||
|
||||
typedef QList<SqlRow> SqlRowList;
|
||||
|
||||
#endif
|
||||
|
||||
48
src/config.h.in
Normal file
48
src/config.h.in
Normal file
@@ -0,0 +1,48 @@
|
||||
/* This file is part of Strawberry.
|
||||
|
||||
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 CONFIG_H_IN
|
||||
#define CONFIG_H_IN
|
||||
|
||||
#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}"
|
||||
#define CMAKE_EXECUTABLE_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}"
|
||||
|
||||
#cmakedefine HAVE_GIO
|
||||
#cmakedefine HAVE_DBUS
|
||||
#cmakedefine HAVE_UDISKS2
|
||||
#cmakedefine HAVE_DEVICEKIT
|
||||
#cmakedefine HAVE_IMOBILEDEVICE
|
||||
#cmakedefine HAVE_LIBARCHIVE
|
||||
#cmakedefine HAVE_AUDIOCD
|
||||
#cmakedefine HAVE_LIBGPOD
|
||||
#cmakedefine HAVE_LIBLASTFM
|
||||
#cmakedefine HAVE_LIBLASTFM1
|
||||
#cmakedefine HAVE_LIBMTP
|
||||
#cmakedefine HAVE_LIBPULSE
|
||||
#cmakedefine HAVE_QCA
|
||||
#cmakedefine HAVE_SPARKLE
|
||||
#cmakedefine IMOBILEDEVICE_USES_UDIDS
|
||||
#cmakedefine TAGLIB_HAS_OPUS
|
||||
#cmakedefine USE_INSTALL_PREFIX
|
||||
#cmakedefine USE_SYSTEM_SHA2
|
||||
|
||||
#cmakedefine HAVE_GSTREAMER
|
||||
#cmakedefine HAVE_VLC
|
||||
#cmakedefine HAVE_XINE
|
||||
#cmakedefine HAVE_PHONON
|
||||
|
||||
#endif // CONFIG_H_IN
|
||||
|
||||
171
src/core/SBSystemPreferences.h
Normal file
171
src/core/SBSystemPreferences.h
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* SBSystemPreferences.h
|
||||
*
|
||||
* Generated with:
|
||||
* sdef "/Applications/System Preferences.app" | sdp -fh --basename
|
||||
*SBSystemPreferences -o SBSystemPreferences.h
|
||||
*/
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
||||
@class SBSystemPreferencesApplication, SBSystemPreferencesDocument,
|
||||
SBSystemPreferencesWindow, SBSystemPreferencesPane,
|
||||
SBSystemPreferencesAnchor;
|
||||
|
||||
enum SBSystemPreferencesSaveOptions {
|
||||
SBSystemPreferencesSaveOptionsYes = 'yes ' /* Save the file. */,
|
||||
SBSystemPreferencesSaveOptionsNo = 'no ' /* Do not save the file. */,
|
||||
SBSystemPreferencesSaveOptionsAsk =
|
||||
'ask ' /* Ask the user whether or not to save the file. */
|
||||
};
|
||||
typedef enum SBSystemPreferencesSaveOptions SBSystemPreferencesSaveOptions;
|
||||
|
||||
enum SBSystemPreferencesPrintingErrorHandling {
|
||||
SBSystemPreferencesPrintingErrorHandlingStandard =
|
||||
'lwst' /* Standard PostScript error handling */,
|
||||
SBSystemPreferencesPrintingErrorHandlingDetailed =
|
||||
'lwdt' /* print a detailed report of PostScript errors */
|
||||
};
|
||||
typedef enum SBSystemPreferencesPrintingErrorHandling
|
||||
SBSystemPreferencesPrintingErrorHandling;
|
||||
|
||||
/*
|
||||
* Standard Suite
|
||||
*/
|
||||
|
||||
// The application's top-level scripting object.
|
||||
@interface SBSystemPreferencesApplication : SBApplication
|
||||
|
||||
- (SBElementArray*)documents;
|
||||
- (SBElementArray*)windows;
|
||||
|
||||
@property(copy, readonly) NSString* name; // The name of the application.
|
||||
@property(readonly) BOOL frontmost; // Is this the active application?
|
||||
@property(copy, readonly)
|
||||
NSString* version; // The version number of the application.
|
||||
|
||||
- (id)open:(id)x; // Open a document.
|
||||
- (void)print:(id)x
|
||||
withProperties:(NSDictionary*)withProperties
|
||||
printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void)quitSaving:
|
||||
(SBSystemPreferencesSaveOptions)saving; // Quit the application.
|
||||
- (BOOL)exists:(id)x; // Verify that an object exists.
|
||||
|
||||
@end
|
||||
|
||||
// A document.
|
||||
@interface SBSystemPreferencesDocument : SBObject
|
||||
|
||||
@property(copy, readonly) NSString* name; // Its name.
|
||||
@property(readonly) BOOL modified; // Has it been modified since the last save?
|
||||
@property(copy, readonly) NSURL* file; // Its location on disk, if it has one.
|
||||
|
||||
- (void)closeSaving:(SBSystemPreferencesSaveOptions)saving
|
||||
savingIn:(NSURL*)savingIn; // Close a document.
|
||||
- (void)saveIn:(NSURL*)in_ as:(id)as; // Save a document.
|
||||
- (void)printWithProperties:(NSDictionary*)withProperties
|
||||
printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject*)to
|
||||
withProperties:(NSDictionary*)withProperties; // Copy an object.
|
||||
- (void)moveTo:(SBObject*)to; // Move an object to a new location.
|
||||
|
||||
@end
|
||||
|
||||
// A window.
|
||||
@interface SBSystemPreferencesWindow : SBObject
|
||||
|
||||
@property(copy, readonly) NSString* name; // The title of the window.
|
||||
- (NSInteger)id; // The unique identifier of the window.
|
||||
@property NSInteger index; // The index of the window, ordered front to back.
|
||||
@property NSRect bounds; // The bounding rectangle of the window.
|
||||
@property(readonly) BOOL closeable; // Does the window have a close button?
|
||||
@property(readonly)
|
||||
BOOL miniaturizable; // Does the window have a minimize button?
|
||||
@property BOOL miniaturized; // Is the window minimized right now?
|
||||
@property(readonly) BOOL resizable; // Can the window be resized?
|
||||
@property BOOL visible; // Is the window visible right now?
|
||||
@property(readonly) BOOL zoomable; // Does the window have a zoom button?
|
||||
@property BOOL zoomed; // Is the window zoomed right now?
|
||||
@property(copy, readonly) SBSystemPreferencesDocument*
|
||||
document; // The document whose contents are displayed in the window.
|
||||
|
||||
- (void)closeSaving:(SBSystemPreferencesSaveOptions)saving
|
||||
savingIn:(NSURL*)savingIn; // Close a document.
|
||||
- (void)saveIn:(NSURL*)in_ as:(id)as; // Save a document.
|
||||
- (void)printWithProperties:(NSDictionary*)withProperties
|
||||
printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject*)to
|
||||
withProperties:(NSDictionary*)withProperties; // Copy an object.
|
||||
- (void)moveTo:(SBObject*)to; // Move an object to a new location.
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* System Preferences
|
||||
*/
|
||||
|
||||
// System Preferences top level scripting object
|
||||
@interface SBSystemPreferencesApplication (SystemPreferences)
|
||||
|
||||
- (SBElementArray*)panes;
|
||||
|
||||
@property(copy)
|
||||
SBSystemPreferencesPane* currentPane; // the currently selected pane
|
||||
@property(copy, readonly) SBSystemPreferencesWindow*
|
||||
preferencesWindow; // the main preferences window
|
||||
@property BOOL showAll; // Is SystemPrefs in show all view. (Setting to false
|
||||
// will do nothing)
|
||||
|
||||
@end
|
||||
|
||||
// a preference pane
|
||||
@interface SBSystemPreferencesPane : SBObject
|
||||
|
||||
- (SBElementArray*)anchors;
|
||||
|
||||
- (NSString*)id; // locale independent name of the preference pane; can refer
|
||||
// to a pane using the expression: pane id "<name>"
|
||||
@property(copy, readonly)
|
||||
NSString* localizedName; // localized name of the preference pane
|
||||
@property(copy, readonly) NSString* name; // name of the preference pane as it
|
||||
// appears in the title bar; can
|
||||
// refer to a pane using the
|
||||
// expression: pane "<name>"
|
||||
|
||||
- (void)closeSaving:(SBSystemPreferencesSaveOptions)saving
|
||||
savingIn:(NSURL*)savingIn; // Close a document.
|
||||
- (void)saveIn:(NSURL*)in_ as:(id)as; // Save a document.
|
||||
- (void)printWithProperties:(NSDictionary*)withProperties
|
||||
printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject*)to
|
||||
withProperties:(NSDictionary*)withProperties; // Copy an object.
|
||||
- (void)moveTo:(SBObject*)to; // Move an object to a new location.
|
||||
- (id)reveal; // Reveals an anchor within a preference pane or preference pane
|
||||
// itself
|
||||
|
||||
@end
|
||||
|
||||
// an anchor within a preference pane
|
||||
@interface SBSystemPreferencesAnchor : SBObject
|
||||
|
||||
@property(copy, readonly)
|
||||
NSString* name; // name of the anchor within a preference pane
|
||||
|
||||
- (void)closeSaving:(SBSystemPreferencesSaveOptions)saving
|
||||
savingIn:(NSURL*)savingIn; // Close a document.
|
||||
- (void)saveIn:(NSURL*)in_ as:(id)as; // Save a document.
|
||||
- (void)printWithProperties:(NSDictionary*)withProperties
|
||||
printDialog:(BOOL)printDialog; // Print a document.
|
||||
- (void) delete; // Delete an object.
|
||||
- (void)duplicateTo:(SBObject*)to
|
||||
withProperties:(NSDictionary*)withProperties; // Copy an object.
|
||||
- (void)moveTo:(SBObject*)to; // Move an object to a new location.
|
||||
- (id)reveal; // Reveals an anchor within a preference pane or preference pane
|
||||
// itself
|
||||
|
||||
@end
|
||||
91
src/core/appearance.cpp
Normal file
91
src/core/appearance.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 "appearance.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QSettings>
|
||||
|
||||
#include "settings/appearancesettingspage.h"
|
||||
|
||||
const char *Appearance::kUseCustomColorSet = "use-custom-set";
|
||||
const char *Appearance::kForegroundColor = "foreground-color";
|
||||
const char *Appearance::kBackgroundColor = "background-color";
|
||||
|
||||
const QPalette Appearance::kDefaultPalette = QPalette();
|
||||
|
||||
Appearance::Appearance(QObject *parent) : QObject(parent) {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
QPalette p = QApplication::palette();
|
||||
background_color_ = s.value(kBackgroundColor, p.color(QPalette::WindowText)).value<QColor>();
|
||||
foreground_color_ = s.value(kForegroundColor, p.color(QPalette::Window)).value<QColor>();
|
||||
|
||||
}
|
||||
|
||||
void Appearance::LoadUserTheme() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(AppearanceSettingsPage::kSettingsGroup);
|
||||
bool use_a_custom_color_set = s.value(kUseCustomColorSet).toBool();
|
||||
if (!use_a_custom_color_set) return;
|
||||
|
||||
ChangeForegroundColor(foreground_color_);
|
||||
ChangeBackgroundColor(background_color_);
|
||||
|
||||
}
|
||||
|
||||
void Appearance::ResetToSystemDefaultTheme() {
|
||||
QApplication::setPalette(kDefaultPalette);
|
||||
}
|
||||
|
||||
void Appearance::ChangeForegroundColor(const QColor &color) {
|
||||
|
||||
// Get the application palette
|
||||
QPalette p = QApplication::palette();
|
||||
|
||||
// Modify the palette
|
||||
p.setColor(QPalette::WindowText, color);
|
||||
p.setColor(QPalette::Text, color);
|
||||
|
||||
// Make the modified palette the new application's palette
|
||||
QApplication::setPalette(p);
|
||||
foreground_color_ = color;
|
||||
|
||||
}
|
||||
|
||||
void Appearance::ChangeBackgroundColor(const QColor &color) {
|
||||
|
||||
// Get the application palette
|
||||
QPalette p = QApplication::palette();
|
||||
|
||||
// Modify the palette
|
||||
p.setColor(QPalette::Window, color);
|
||||
p.setColor(QPalette::Base, color);
|
||||
|
||||
// Make the modified palette the new application's palette
|
||||
QApplication::setPalette(p);
|
||||
background_color_ = color;
|
||||
|
||||
}
|
||||
|
||||
51
src/core/appearance.h
Normal file
51
src/core/appearance.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 APPEARANCE_H
|
||||
#define APPEARANCE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QPalette>
|
||||
|
||||
class Appearance : public QObject {
|
||||
public:
|
||||
explicit Appearance(QObject* parent = nullptr);
|
||||
// Load the user preferred theme, which could the default system theme or a
|
||||
// custom set of colors that user has chosen
|
||||
void LoadUserTheme();
|
||||
void ResetToSystemDefaultTheme();
|
||||
void ChangeForegroundColor(const QColor& color);
|
||||
void ChangeBackgroundColor(const QColor& color);
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
static const char* kUseCustomColorSet;
|
||||
static const char* kForegroundColor;
|
||||
static const char* kBackgroundColor;
|
||||
static const QPalette kDefaultPalette;
|
||||
|
||||
private:
|
||||
QColor foreground_color_;
|
||||
QColor background_color_;
|
||||
};
|
||||
|
||||
#endif // APPEARANCE_H
|
||||
|
||||
217
src/core/application.cpp
Normal file
217
src/core/application.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 "application.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "core/appearance.h"
|
||||
#include "core/database.h"
|
||||
#include "core/lazy.h"
|
||||
#include "core/player.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "engine/enginetype.h"
|
||||
#include "engine/enginedevice.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collection.h"
|
||||
#include "playlist/playlistbackend.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "covermanager/coverproviders.h"
|
||||
#include "covermanager/currentartloader.h"
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
#include "covermanager/lastfmcoverprovider.h"
|
||||
#endif // HAVE_LIBLASTFM
|
||||
#include "covermanager/amazoncoverprovider.h"
|
||||
#include "covermanager/discogscoverprovider.h"
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
|
||||
bool Application::kIsPortable = false;
|
||||
|
||||
class ApplicationImpl {
|
||||
public:
|
||||
ApplicationImpl(Application *app) :
|
||||
tag_reader_client_([=]() {
|
||||
TagReaderClient *client = new TagReaderClient(app);
|
||||
app->MoveToNewThread(client);
|
||||
client->Start();
|
||||
return client;
|
||||
}),
|
||||
database_([=]() {
|
||||
Database *db = new Database(app, app);
|
||||
app->MoveToNewThread(db);
|
||||
DoInAMinuteOrSo(db, SLOT(DoBackup()));
|
||||
return db;
|
||||
}),
|
||||
appearance_([=]() { return new Appearance(app); }),
|
||||
task_manager_([=]() { return new TaskManager(app); }),
|
||||
player_([=]() { return new Player(app, app); }),
|
||||
enginedevice_([=]() { return new EngineDevice(app); }),
|
||||
device_manager_([=]() { return new DeviceManager(app, app); }),
|
||||
collection_([=]() { return new Collection(app, app); }),
|
||||
playlist_backend_([=]() {
|
||||
PlaylistBackend *backend = new PlaylistBackend(app, app);
|
||||
app->MoveToThread(backend, database_->thread());
|
||||
return backend;
|
||||
}),
|
||||
playlist_manager_([=]() { return new PlaylistManager(app); }),
|
||||
cover_providers_([=]() {
|
||||
CoverProviders *cover_providers = new CoverProviders(app);
|
||||
// Initialize the repository of cover providers.
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
cover_providers->AddProvider(new LastFmCoverProvider(app));
|
||||
#endif
|
||||
cover_providers->AddProvider(new AmazonCoverProvider(app));
|
||||
cover_providers->AddProvider(new DiscogsCoverProvider(app));
|
||||
cover_providers->AddProvider(new MusicbrainzCoverProvider(app));
|
||||
return cover_providers;
|
||||
}),
|
||||
album_cover_loader_([=]() {
|
||||
AlbumCoverLoader *loader = new AlbumCoverLoader(app);
|
||||
app->MoveToNewThread(loader);
|
||||
return loader;
|
||||
}),
|
||||
current_art_loader_([=]() { return new CurrentArtLoader(app, app); })
|
||||
{ }
|
||||
|
||||
Lazy<TagReaderClient> tag_reader_client_;
|
||||
Lazy<Database> database_;
|
||||
Lazy<Appearance> appearance_;
|
||||
Lazy<TaskManager> task_manager_;
|
||||
Lazy<Player> player_;
|
||||
Lazy<EngineDevice> enginedevice_;
|
||||
Lazy<DeviceManager> device_manager_;
|
||||
Lazy<Collection> collection_;
|
||||
Lazy<PlaylistBackend> playlist_backend_;
|
||||
Lazy<PlaylistManager> playlist_manager_;
|
||||
Lazy<CoverProviders> cover_providers_;
|
||||
Lazy<AlbumCoverLoader> album_cover_loader_;
|
||||
Lazy<CurrentArtLoader> current_art_loader_;
|
||||
|
||||
};
|
||||
|
||||
Application::Application(QObject *parent)
|
||||
: QObject(parent), p_(new ApplicationImpl(this)) {
|
||||
|
||||
enginedevice()->Init();
|
||||
collection()->Init();
|
||||
tag_reader_client();
|
||||
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
|
||||
// It's important that the device manager is deleted before the database.
|
||||
// Deleting the database deletes all objects that have been created in its
|
||||
// thread, including some device collection backends.
|
||||
p_->device_manager_.reset();
|
||||
|
||||
for (QThread *thread : threads_) {
|
||||
thread->quit();
|
||||
}
|
||||
|
||||
for (QThread *thread : threads_) {
|
||||
thread->wait();
|
||||
}
|
||||
}
|
||||
|
||||
void Application::MoveToNewThread(QObject *object) {
|
||||
|
||||
QThread *thread = new QThread(this);
|
||||
|
||||
MoveToThread(object, thread);
|
||||
|
||||
thread->start();
|
||||
threads_ << thread;
|
||||
}
|
||||
|
||||
void Application::MoveToThread(QObject *object, QThread *thread) {
|
||||
object->setParent(nullptr);
|
||||
object->moveToThread(thread);
|
||||
}
|
||||
|
||||
void Application::AddError(const QString& message) { emit ErrorAdded(message); }
|
||||
|
||||
QString Application::language_without_region() const {
|
||||
const int underscore = language_name_.indexOf('_');
|
||||
if (underscore != -1) {
|
||||
return language_name_.left(underscore);
|
||||
}
|
||||
return language_name_;
|
||||
}
|
||||
|
||||
void Application::ReloadSettings() { emit SettingsChanged(); }
|
||||
|
||||
void Application::OpenSettingsDialogAtPage(SettingsDialog::Page page) {
|
||||
emit SettingsDialogRequested(page);
|
||||
}
|
||||
|
||||
AlbumCoverLoader *Application::album_cover_loader() const {
|
||||
return p_->album_cover_loader_.get();
|
||||
}
|
||||
|
||||
Appearance *Application::appearance() const { return p_->appearance_.get(); }
|
||||
|
||||
CoverProviders *Application::cover_providers() const {
|
||||
return p_->cover_providers_.get();
|
||||
}
|
||||
|
||||
CurrentArtLoader *Application::current_art_loader() const {
|
||||
return p_->current_art_loader_.get();
|
||||
}
|
||||
|
||||
Database *Application::database() const { return p_->database_.get(); }
|
||||
|
||||
DeviceManager *Application::device_manager() const {
|
||||
return p_->device_manager_.get();
|
||||
}
|
||||
|
||||
Collection *Application::collection() const { return p_->collection_.get(); }
|
||||
|
||||
CollectionBackend *Application::collection_backend() const {
|
||||
return collection()->backend();
|
||||
}
|
||||
|
||||
CollectionModel *Application::collection_model() const { return collection()->model(); }
|
||||
|
||||
Player *Application::player() const { return p_->player_.get(); }
|
||||
|
||||
PlaylistBackend *Application::playlist_backend() const {
|
||||
return p_->playlist_backend_.get();
|
||||
}
|
||||
|
||||
PlaylistManager *Application::playlist_manager() const {
|
||||
return p_->playlist_manager_.get();
|
||||
}
|
||||
|
||||
TagReaderClient *Application::tag_reader_client() const {
|
||||
return p_->tag_reader_client_.get();
|
||||
}
|
||||
|
||||
TaskManager *Application::task_manager() const {
|
||||
return p_->task_manager_.get();
|
||||
}
|
||||
|
||||
EngineDevice *Application::enginedevice() const {
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
return p_->enginedevice_.get();
|
||||
}
|
||||
103
src/core/application.h
Normal file
103
src/core/application.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 APPLICATION_H_
|
||||
#define APPLICATION_H_
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
class ApplicationImpl;
|
||||
class TagReaderClient;
|
||||
class Database;
|
||||
class Appearance;
|
||||
class TaskManager;
|
||||
class Player;
|
||||
class DeviceManager;
|
||||
class Collection;
|
||||
class PlaylistBackend;
|
||||
class PlaylistManager;
|
||||
class AlbumCoverLoader;
|
||||
class CoverProviders;
|
||||
class CurrentArtLoader;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
class EngineDevice;
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static bool kIsPortable;
|
||||
|
||||
explicit Application(QObject *parent = nullptr);
|
||||
~Application();
|
||||
|
||||
const QString &language_name() const { return language_name_; }
|
||||
// Same as language_name, but remove the region code at the end if there is one
|
||||
QString language_without_region() const;
|
||||
void set_language_name(const QString &name) { language_name_ = name; }
|
||||
|
||||
TagReaderClient *tag_reader_client() const;
|
||||
Database *database() const;
|
||||
Appearance *appearance() const;
|
||||
TaskManager *task_manager() const;
|
||||
Player *player() const;
|
||||
EngineDevice *enginedevice() const;
|
||||
DeviceManager *device_manager() const;
|
||||
|
||||
Collection *collection() const;
|
||||
|
||||
PlaylistBackend *playlist_backend() const;
|
||||
PlaylistManager *playlist_manager() const;
|
||||
|
||||
CoverProviders *cover_providers() const;
|
||||
AlbumCoverLoader *album_cover_loader() const;
|
||||
CurrentArtLoader *current_art_loader() const;
|
||||
|
||||
CollectionBackend *collection_backend() const;
|
||||
CollectionModel *collection_model() const;
|
||||
|
||||
void MoveToNewThread(QObject *object);
|
||||
void MoveToThread(QObject *object, QThread *thread);
|
||||
|
||||
public slots:
|
||||
void AddError(const QString &message);
|
||||
void ReloadSettings();
|
||||
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
|
||||
|
||||
signals:
|
||||
void ErrorAdded(const QString &message);
|
||||
void SettingsChanged();
|
||||
void SettingsDialogRequested(SettingsDialog::Page page);
|
||||
|
||||
private:
|
||||
QString language_name_;
|
||||
std::unique_ptr<ApplicationImpl> p_;
|
||||
QList<QThread*> threads_;
|
||||
|
||||
};
|
||||
|
||||
#endif // APPLICATION_H_
|
||||
103
src/core/cachedlist.h
Normal file
103
src/core/cachedlist.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2011, 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 CACHEDLIST_H
|
||||
#define CACHEDLIST_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QSettings>
|
||||
|
||||
template <typename T>
|
||||
class CachedList {
|
||||
public:
|
||||
// Use a CachedList when you want to download and save a list of things from a
|
||||
// remote service, updating it only periodically.
|
||||
// T must be a registered metatype and must support being stored in
|
||||
// QSettings. This usually means you have to implement QDataStream streaming
|
||||
// operators, and use qRegisterMetaTypeStreamOperators.
|
||||
|
||||
typedef QList<T> ListType;
|
||||
|
||||
CachedList(const QString &settings_group, const QString &name, int cache_duration_secs)
|
||||
: settings_group_(settings_group), name_(name), cache_duration_secs_(cache_duration_secs) {
|
||||
}
|
||||
|
||||
void Load() {
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
|
||||
last_updated_ = s.value("last_refreshed_" + name_).toDateTime();
|
||||
data_.clear();
|
||||
|
||||
const int count = s.beginReadArray(name_ + "_data");
|
||||
for (int i = 0; i < count; ++i) {
|
||||
s.setArrayIndex(i);
|
||||
data_ << s.value("value").value<T>();
|
||||
}
|
||||
s.endArray();
|
||||
}
|
||||
|
||||
void Save() const {
|
||||
QSettings s;
|
||||
s.beginGroup(settings_group_);
|
||||
|
||||
s.setValue("last_refreshed_" + name_, last_updated_);
|
||||
|
||||
s.beginWriteArray(name_ + "_data", data_.size());
|
||||
for (int i = 0; i < data_.size(); ++i) {
|
||||
s.setArrayIndex(i);
|
||||
s.setValue("value", QVariant::fromValue(data_[i]));
|
||||
}
|
||||
s.endArray();
|
||||
}
|
||||
|
||||
void Update(const ListType &data) {
|
||||
data_ = data;
|
||||
last_updated_ = QDateTime::currentDateTime();
|
||||
Save();
|
||||
}
|
||||
|
||||
bool IsStale() const {
|
||||
return last_updated_.isNull() || last_updated_.secsTo(QDateTime::currentDateTime()) > cache_duration_secs_;
|
||||
}
|
||||
|
||||
void Sort() { qSort(data_); }
|
||||
|
||||
const ListType &Data() const { return data_; }
|
||||
operator ListType() const { return data_; }
|
||||
|
||||
// Q_FOREACH support
|
||||
typedef typename ListType::const_iterator const_iterator;
|
||||
const_iterator begin() const { return data_.begin(); }
|
||||
const_iterator end() const { return data_.end(); }
|
||||
|
||||
private:
|
||||
const QString settings_group_;
|
||||
const QString name_;
|
||||
const int cache_duration_secs_;
|
||||
|
||||
QDateTime last_updated_;
|
||||
ListType data_;
|
||||
};
|
||||
|
||||
#endif // CACHEDLIST_H
|
||||
|
||||
388
src/core/commandlineoptions.cpp
Normal file
388
src/core/commandlineoptions.cpp
Normal file
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 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 "commandlineoptions.h"
|
||||
#include "version.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <getopt.h>
|
||||
#include <iostream>
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QCoreApplication>
|
||||
#include <QFileInfo>
|
||||
|
||||
|
||||
const char *CommandlineOptions::kHelpText =
|
||||
"%1: strawberry [%2] [%3]\n"
|
||||
"\n"
|
||||
"%4:\n"
|
||||
" -p, --play %5\n"
|
||||
" -t, --play-pause %6\n"
|
||||
" -u, --pause %7\n"
|
||||
" -s, --stop %8\n"
|
||||
" -q, --stop-after-current %9\n"
|
||||
" -r, --previous %10\n"
|
||||
" -f, --next %11\n"
|
||||
" -v, --volume <value> %12\n"
|
||||
" --volume-up %13\n"
|
||||
" --volume-down %14\n"
|
||||
" --volume-increase-by %15\n"
|
||||
" --volume-decrease-by %16\n"
|
||||
" --seek-to <seconds> %17\n"
|
||||
" --seek-by <seconds> %18\n"
|
||||
" --restart-or-previous %19\n"
|
||||
"\n"
|
||||
"%20:\n"
|
||||
" -c, --create <name> %21\n"
|
||||
" -a, --append %22\n"
|
||||
" -l, --load %23\n"
|
||||
" -k, --play-track <n> %24\n"
|
||||
"\n"
|
||||
"%25:\n"
|
||||
" -o, --show-osd %26\n"
|
||||
" -y, --toggle-pretty-osd %27\n"
|
||||
" -g, --language <lang> %28\n"
|
||||
" --quiet %29\n"
|
||||
" --verbose %30\n"
|
||||
" --log-levels <levels> %31\n"
|
||||
" --version %32\n";
|
||||
|
||||
const char *CommandlineOptions::kVersionText = "Strawberry %1";
|
||||
|
||||
CommandlineOptions::CommandlineOptions(int argc, char* *argv)
|
||||
: argc_(argc),
|
||||
argv_(argv),
|
||||
url_list_action_(UrlList_None),
|
||||
player_action_(Player_None),
|
||||
set_volume_(-1),
|
||||
volume_modifier_(0),
|
||||
seek_to_(-1),
|
||||
seek_by_(0),
|
||||
play_track_at_(-1),
|
||||
show_osd_(false),
|
||||
toggle_pretty_osd_(false),
|
||||
log_levels_(logging::kDefaultLogLevels) {
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Remove -psn_xxx option that Mac passes when opened from Finder.
|
||||
RemoveArg("-psn", 1);
|
||||
#endif
|
||||
|
||||
// Remove the -session option that KDE passes
|
||||
RemoveArg("-session", 2);
|
||||
}
|
||||
|
||||
void CommandlineOptions::RemoveArg(const QString& starts_with, int count) {
|
||||
|
||||
for (int i = 0; i < argc_; ++i) {
|
||||
QString opt(argv_[i]);
|
||||
if (opt.startsWith(starts_with)) {
|
||||
for (int j = i; j < argc_ - count + 1; ++j) {
|
||||
argv_[j] = argv_[j + count];
|
||||
}
|
||||
argc_ -= count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool CommandlineOptions::Parse() {
|
||||
|
||||
static const struct option kOptions[] = {
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{"play", no_argument, 0, 'p'},
|
||||
{"play-pause", no_argument, 0, 't'},
|
||||
{"pause", no_argument, 0, 'u'},
|
||||
{"stop", no_argument, 0, 's'},
|
||||
{"stop-after-current", no_argument, 0, 'q'},
|
||||
{"previous", no_argument, 0, 'r'},
|
||||
{"next", no_argument, 0, 'f'},
|
||||
{"volume", required_argument, 0, 'v'},
|
||||
{"volume-up", no_argument, 0, VolumeUp},
|
||||
{"volume-down", no_argument, 0, VolumeDown},
|
||||
{"volume-increase-by", required_argument, 0, VolumeIncreaseBy},
|
||||
{"volume-decrease-by", required_argument, 0, VolumeDecreaseBy},
|
||||
{"seek-to", required_argument, 0, SeekTo},
|
||||
{"seek-by", required_argument, 0, SeekBy},
|
||||
{"restart-or-previous", no_argument, 0, RestartOrPrevious},
|
||||
{"create", required_argument, 0, 'c'},
|
||||
{"append", no_argument, 0, 'a'},
|
||||
{"load", no_argument, 0, 'l'},
|
||||
{"play-track", required_argument, 0, 'k'},
|
||||
{"show-osd", no_argument, 0, 'o'},
|
||||
{"toggle-pretty-osd", no_argument, 0, 'y'},
|
||||
{"language", required_argument, 0, 'g'},
|
||||
{"quiet", no_argument, 0, Quiet},
|
||||
{"verbose", no_argument, 0, Verbose},
|
||||
{"log-levels", required_argument, 0, LogLevels},
|
||||
{"version", no_argument, 0, Version},
|
||||
{0, 0, 0, 0}};
|
||||
|
||||
// Parse the arguments
|
||||
bool ok = false;
|
||||
forever {
|
||||
int c = getopt_long(argc_, argv_, "hptusqrfv:c:alk:oyg:", kOptions, nullptr);
|
||||
|
||||
// End of the options
|
||||
if (c == -1) break;
|
||||
|
||||
switch (c) {
|
||||
case 'h': {
|
||||
QString translated_help_text =
|
||||
QString(kHelpText)
|
||||
.arg(tr("Usage"), tr("options"), tr("URL(s)"),
|
||||
tr("Player options"),
|
||||
tr("Start the playlist currently playing"),
|
||||
tr("Play if stopped, pause if playing"),
|
||||
tr("Pause playback"), tr("Stop playback"),
|
||||
tr("Stop playback after current track"))
|
||||
.arg(tr("Skip backwards in playlist"),
|
||||
tr("Skip forwards in playlist"),
|
||||
tr("Set the volume to <value> percent"),
|
||||
tr("Increase the volume by 4%"),
|
||||
tr("Decrease the volume by 4%"),
|
||||
tr("Increase the volume by <value> percent"),
|
||||
tr("Decrease the volume by <value> percent"))
|
||||
.arg(tr("Seek the currently playing track to an absolute "
|
||||
"position"),
|
||||
tr("Seek the currently playing track by a relative "
|
||||
"amount"),
|
||||
tr("Restart the track, or play the previous track if "
|
||||
"within 8 seconds of start."),
|
||||
tr("Playlist options"),
|
||||
tr("Create a new playlist with files"),
|
||||
tr("Append files/URLs to the playlist"),
|
||||
tr("Loads files/URLs, replacing current playlist"),
|
||||
tr("Play the <n>th track in the playlist"))
|
||||
.arg(tr("Other options"), tr("Display the on-screen-display"),
|
||||
tr("Toggle visibility for the pretty on-screen-display"),
|
||||
tr("Change the language"),
|
||||
tr("Equivalent to --log-levels *:1"),
|
||||
tr("Equivalent to --log-levels *:3"),
|
||||
tr("Comma separated list of class:level, level is 0-3"))
|
||||
.arg(tr("Print out version information"));
|
||||
|
||||
std::cout << translated_help_text.toLocal8Bit().constData();
|
||||
return false;
|
||||
}
|
||||
|
||||
case 'p':
|
||||
player_action_ = Player_Play;
|
||||
break;
|
||||
case 't':
|
||||
player_action_ = Player_PlayPause;
|
||||
break;
|
||||
case 'u':
|
||||
player_action_ = Player_Pause;
|
||||
break;
|
||||
case 's':
|
||||
player_action_ = Player_Stop;
|
||||
break;
|
||||
case 'q':
|
||||
player_action_ = Player_StopAfterCurrent;
|
||||
break;
|
||||
case 'r':
|
||||
player_action_ = Player_Previous;
|
||||
break;
|
||||
case 'f':
|
||||
player_action_ = Player_Next;
|
||||
break;
|
||||
case 'c':
|
||||
url_list_action_ = UrlList_CreateNew;
|
||||
playlist_name_ = QString(optarg);
|
||||
break;
|
||||
case 'a':
|
||||
url_list_action_ = UrlList_Append;
|
||||
break;
|
||||
case 'l':
|
||||
url_list_action_ = UrlList_Load;
|
||||
break;
|
||||
case 'o':
|
||||
show_osd_ = true;
|
||||
break;
|
||||
case 'y':
|
||||
toggle_pretty_osd_ = true;
|
||||
break;
|
||||
case 'g':
|
||||
language_ = QString(optarg);
|
||||
break;
|
||||
case VolumeUp:
|
||||
volume_modifier_ = +4;
|
||||
break;
|
||||
case VolumeDown:
|
||||
volume_modifier_ = -4;
|
||||
break;
|
||||
case Quiet:
|
||||
log_levels_ = "1";
|
||||
break;
|
||||
case Verbose:
|
||||
log_levels_ = "3";
|
||||
break;
|
||||
case LogLevels:
|
||||
log_levels_ = QString(optarg);
|
||||
break;
|
||||
case Version: {
|
||||
QString version_text = QString(kVersionText).arg(STRAWBERRY_VERSION_DISPLAY);
|
||||
std::cout << version_text.toLocal8Bit().constData() << std::endl;
|
||||
std::exit(0);
|
||||
}
|
||||
case 'v':
|
||||
set_volume_ = QString(optarg).toInt(&ok);
|
||||
if (!ok) set_volume_ = -1;
|
||||
break;
|
||||
|
||||
case VolumeIncreaseBy:
|
||||
volume_modifier_ = QString(optarg).toInt(&ok);
|
||||
if (!ok) volume_modifier_ = 0;
|
||||
break;
|
||||
|
||||
case VolumeDecreaseBy:
|
||||
volume_modifier_ = -QString(optarg).toInt(&ok);
|
||||
if (!ok) volume_modifier_ = 0;
|
||||
break;
|
||||
|
||||
case SeekTo:
|
||||
seek_to_ = QString(optarg).toInt(&ok);
|
||||
if (!ok) seek_to_ = -1;
|
||||
break;
|
||||
|
||||
case SeekBy:
|
||||
seek_by_ = QString(optarg).toInt(&ok);
|
||||
if (!ok) seek_by_ = 0;
|
||||
break;
|
||||
|
||||
case RestartOrPrevious:
|
||||
player_action_ = Player_RestartOrPrevious;
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
play_track_at_ = QString(optarg).toInt(&ok);
|
||||
if (!ok) play_track_at_ = -1;
|
||||
break;
|
||||
|
||||
case '?':
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get any filenames or URLs following the arguments
|
||||
for (int i = optind; i < argc_; ++i) {
|
||||
QString value = QFile::decodeName(argv_[i]);
|
||||
QFileInfo file_info(value);
|
||||
if (file_info.exists())
|
||||
urls_ << QUrl::fromLocalFile(file_info.canonicalFilePath());
|
||||
else
|
||||
urls_ << QUrl::fromUserInput(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool CommandlineOptions::is_empty() const {
|
||||
return player_action_ == Player_None &&
|
||||
set_volume_ == -1 &&
|
||||
volume_modifier_ == 0 &&
|
||||
seek_to_ == -1 &&
|
||||
seek_by_ == 0 &&
|
||||
play_track_at_ == -1 &&
|
||||
show_osd_ == false &&
|
||||
toggle_pretty_osd_ == false &&
|
||||
urls_.isEmpty();
|
||||
}
|
||||
|
||||
bool CommandlineOptions::contains_play_options() const {
|
||||
return player_action_ != Player_None || play_track_at_ != -1 || !urls_.isEmpty();
|
||||
}
|
||||
|
||||
QByteArray CommandlineOptions::Serialize() const {
|
||||
|
||||
QBuffer buf;
|
||||
buf.open(QIODevice::WriteOnly);
|
||||
|
||||
QDataStream s(&buf);
|
||||
s << *this;
|
||||
buf.close();
|
||||
|
||||
return buf.data().toBase64();
|
||||
|
||||
}
|
||||
|
||||
void CommandlineOptions::Load(const QByteArray &serialized) {
|
||||
|
||||
QByteArray copy = QByteArray::fromBase64(serialized);
|
||||
QBuffer buf(©);
|
||||
buf.open(QIODevice::ReadOnly);
|
||||
|
||||
QDataStream s(&buf);
|
||||
s >> *this;
|
||||
|
||||
}
|
||||
|
||||
QString CommandlineOptions::tr(const char *source_text) {
|
||||
return QObject::tr(source_text);
|
||||
}
|
||||
|
||||
QDataStream& operator<<(QDataStream &s, const CommandlineOptions &a) {
|
||||
|
||||
s << qint32(a.player_action_)
|
||||
<< qint32(a.url_list_action_)
|
||||
<< a.set_volume_
|
||||
<< a.volume_modifier_
|
||||
<< a.seek_to_
|
||||
<< a.seek_by_
|
||||
<< a.play_track_at_
|
||||
<< a.show_osd_
|
||||
<< a.urls_
|
||||
<< a.log_levels_
|
||||
<< a.toggle_pretty_osd_;
|
||||
|
||||
return s;
|
||||
|
||||
}
|
||||
|
||||
QDataStream& operator>>(QDataStream &s, CommandlineOptions &a) {
|
||||
|
||||
quint32 player_action = 0;
|
||||
quint32 url_list_action = 0;
|
||||
s >> player_action
|
||||
>> url_list_action
|
||||
>> a.set_volume_
|
||||
>> a.volume_modifier_
|
||||
>> a.seek_to_
|
||||
>> a.seek_by_
|
||||
>> a.play_track_at_
|
||||
>> a.show_osd_
|
||||
>> a.urls_
|
||||
>> a.log_levels_
|
||||
>> a.toggle_pretty_osd_;
|
||||
a.player_action_ = CommandlineOptions::PlayerAction(player_action);
|
||||
a.url_list_action_ = CommandlineOptions::UrlListAction(url_list_action);
|
||||
|
||||
return s;
|
||||
|
||||
}
|
||||
|
||||
128
src/core/commandlineoptions.h
Normal file
128
src/core/commandlineoptions.h
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 COMMANDLINEOPTIONS_H
|
||||
#define COMMANDLINEOPTIONS_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
#include <QDataStream>
|
||||
|
||||
class CommandlineOptions {
|
||||
friend QDataStream &operator<<(QDataStream &s, const CommandlineOptions &a);
|
||||
friend QDataStream &operator>>(QDataStream &s, CommandlineOptions &a);
|
||||
|
||||
public:
|
||||
explicit CommandlineOptions(int argc = 0, char **argv = nullptr);
|
||||
|
||||
static const char *kHelpText;
|
||||
static const char *kVersionText;
|
||||
|
||||
// Don't change the values or order, these get serialised and sent to
|
||||
// possibly a different version of Strawberry
|
||||
enum UrlListAction {
|
||||
UrlList_Append = 0,
|
||||
UrlList_Load = 1,
|
||||
UrlList_None = 2,
|
||||
UrlList_CreateNew = 3,
|
||||
};
|
||||
enum PlayerAction {
|
||||
Player_None = 0,
|
||||
Player_Play = 1,
|
||||
Player_PlayPause = 2,
|
||||
Player_Pause = 3,
|
||||
Player_Stop = 4,
|
||||
Player_Previous = 5,
|
||||
Player_Next = 6,
|
||||
Player_RestartOrPrevious = 7,
|
||||
Player_StopAfterCurrent = 8,
|
||||
};
|
||||
|
||||
bool Parse();
|
||||
|
||||
bool is_empty() const;
|
||||
bool contains_play_options() const;
|
||||
|
||||
UrlListAction url_list_action() const { return url_list_action_; }
|
||||
PlayerAction player_action() const { return player_action_; }
|
||||
int set_volume() const { return set_volume_; }
|
||||
int volume_modifier() const { return volume_modifier_; }
|
||||
int seek_to() const { return seek_to_; }
|
||||
int seek_by() const { return seek_by_; }
|
||||
int play_track_at() const { return play_track_at_; }
|
||||
bool show_osd() const { return show_osd_; }
|
||||
bool toggle_pretty_osd() const { return toggle_pretty_osd_; }
|
||||
QList<QUrl> urls() const { return urls_; }
|
||||
QString language() const { return language_; }
|
||||
QString log_levels() const { return log_levels_; }
|
||||
QString playlist_name() const { return playlist_name_; }
|
||||
|
||||
QByteArray Serialize() const;
|
||||
void Load(const QByteArray &serialized);
|
||||
|
||||
private:
|
||||
// These are "invalid" characters to pass to getopt_long for options that
|
||||
// shouldn't have a short (single character) option.
|
||||
enum LongOptions {
|
||||
VolumeUp = 256,
|
||||
VolumeDown,
|
||||
SeekTo,
|
||||
SeekBy,
|
||||
Quiet,
|
||||
Verbose,
|
||||
LogLevels,
|
||||
Version,
|
||||
VolumeIncreaseBy,
|
||||
VolumeDecreaseBy,
|
||||
RestartOrPrevious
|
||||
};
|
||||
|
||||
QString tr(const char *source_text);
|
||||
void RemoveArg(const QString &starts_with, int count);
|
||||
|
||||
private:
|
||||
int argc_;
|
||||
char **argv_;
|
||||
|
||||
UrlListAction url_list_action_;
|
||||
PlayerAction player_action_;
|
||||
|
||||
// Don't change the type of these.
|
||||
int set_volume_;
|
||||
int volume_modifier_;
|
||||
int seek_to_;
|
||||
int seek_by_;
|
||||
int play_track_at_;
|
||||
bool show_osd_;
|
||||
bool toggle_pretty_osd_;
|
||||
QString language_;
|
||||
QString log_levels_;
|
||||
QString playlist_name_;
|
||||
|
||||
QList<QUrl> urls_;
|
||||
};
|
||||
|
||||
QDataStream &operator<<(QDataStream &s, const CommandlineOptions &a);
|
||||
QDataStream &operator>>(QDataStream &s, CommandlineOptions &a);
|
||||
|
||||
#endif // COMMANDLINEOPTIONS_H
|
||||
|
||||
684
src/core/database.cpp
Normal file
684
src/core/database.cpp
Normal file
@@ -0,0 +1,684 @@
|
||||
/*
|
||||
* 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 "database.h"
|
||||
#include "scopedtransaction.h"
|
||||
#include "utilities.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/taskmanager.h"
|
||||
|
||||
#include <boost/scope_exit.hpp>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLibrary>
|
||||
#include <QLibraryInfo>
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlQuery>
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
|
||||
const char *Database::kDatabaseFilename = "strawberry.db";
|
||||
const int Database::kSchemaVersion = 0;
|
||||
const char *Database::kMagicAllSongsTables = "%allsongstables";
|
||||
|
||||
int Database::sNextConnectionId = 1;
|
||||
QMutex Database::sNextConnectionIdMutex;
|
||||
|
||||
Database::Token::Token(const QString &token, int start, int end)
|
||||
: token(token), start_offset(start), end_offset(end) {}
|
||||
|
||||
struct sqlite3_tokenizer_module {
|
||||
|
||||
int iVersion;
|
||||
int (*xCreate)(int argc, /* Size of argv array */
|
||||
const char *const *argv, /* Tokenizer argument strings */
|
||||
sqlite3_tokenizer** ppTokenizer); /* OUT: Created tokenizer */
|
||||
|
||||
int (*xDestroy)(sqlite3_tokenizer *pTokenizer);
|
||||
|
||||
int (*xOpen)(
|
||||
sqlite3_tokenizer *pTokenizer, /* Tokenizer object */
|
||||
const char *pInput, int nBytes, /* Input buffer */
|
||||
sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */
|
||||
);
|
||||
|
||||
int (*xClose)(sqlite3_tokenizer_cursor *pCursor);
|
||||
|
||||
int (*xNext)(
|
||||
sqlite3_tokenizer_cursor *pCursor, /* Tokenizer cursor */
|
||||
const char* *ppToken, int *pnBytes, /* OUT: Normalized text for token */
|
||||
int *piStartOffset, /* OUT: Byte offset of token in input buffer */
|
||||
int *piEndOffset, /* OUT: Byte offset of end of token in input buffer */
|
||||
int *piPosition); /* OUT: Number of tokens returned before this one */
|
||||
};
|
||||
|
||||
struct sqlite3_tokenizer {
|
||||
const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */
|
||||
/* Tokenizer implementations will typically add additional fields */
|
||||
};
|
||||
|
||||
struct sqlite3_tokenizer_cursor {
|
||||
sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */
|
||||
/* Tokenizer implementations will typically add additional fields */
|
||||
};
|
||||
|
||||
sqlite3_tokenizer_module *Database::sFTSTokenizer = nullptr;
|
||||
|
||||
int Database::FTSCreate(int argc, const char *const *argv, sqlite3_tokenizer **tokenizer) {
|
||||
|
||||
*tokenizer = reinterpret_cast<sqlite3_tokenizer*>(new UnicodeTokenizer);
|
||||
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
int Database::FTSDestroy(sqlite3_tokenizer *tokenizer) {
|
||||
|
||||
UnicodeTokenizer *real_tokenizer = reinterpret_cast<UnicodeTokenizer*>(tokenizer);
|
||||
delete real_tokenizer;
|
||||
return SQLITE_OK;
|
||||
}
|
||||
|
||||
int Database::FTSOpen(sqlite3_tokenizer *pTokenizer, const char *input, int bytes, sqlite3_tokenizer_cursor **cursor) {
|
||||
|
||||
UnicodeTokenizerCursor *new_cursor = new UnicodeTokenizerCursor;
|
||||
new_cursor->pTokenizer = pTokenizer;
|
||||
new_cursor->position = 0;
|
||||
|
||||
QString str = QString::fromUtf8(input, bytes).toLower();
|
||||
QChar *data = str.data();
|
||||
// Decompose and strip punctuation.
|
||||
QList<Token> tokens;
|
||||
QString token;
|
||||
int start_offset = 0;
|
||||
int offset = 0;
|
||||
for (int i = 0; i < str.length(); ++i) {
|
||||
QChar c = data[i];
|
||||
ushort unicode = c.unicode();
|
||||
if (unicode <= 0x007f) {
|
||||
offset += 1;
|
||||
}
|
||||
else if (unicode >= 0x0080 && unicode <= 0x07ff) {
|
||||
offset += 2;
|
||||
}
|
||||
else if (unicode >= 0x0800) {
|
||||
offset += 3;
|
||||
}
|
||||
// Unicode astral planes unsupported in Qt?
|
||||
/*else if (unicode >= 0x010000 && unicode <= 0x10ffff) {
|
||||
offset += 4;
|
||||
}*/
|
||||
|
||||
if (!data[i].isLetterOrNumber()) {
|
||||
// Token finished.
|
||||
if (token.length() != 0) {
|
||||
tokens << Token(token, start_offset, offset - 1);
|
||||
start_offset = offset;
|
||||
token.clear();
|
||||
}
|
||||
else {
|
||||
++start_offset;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (data[i].decompositionTag() != QChar::NoDecomposition) {
|
||||
token.push_back(data[i].decomposition()[0]);
|
||||
} else {
|
||||
token.push_back(data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (i == str.length() - 1) {
|
||||
if (token.length() != 0) {
|
||||
tokens << Token(token, start_offset, offset);
|
||||
token.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_cursor->tokens = tokens;
|
||||
*cursor = reinterpret_cast<sqlite3_tokenizer_cursor*>(new_cursor);
|
||||
|
||||
return SQLITE_OK;
|
||||
|
||||
}
|
||||
|
||||
int Database::FTSClose(sqlite3_tokenizer_cursor *cursor) {
|
||||
|
||||
UnicodeTokenizerCursor *real_cursor = reinterpret_cast<UnicodeTokenizerCursor*>(cursor);
|
||||
delete real_cursor;
|
||||
|
||||
return SQLITE_OK;
|
||||
|
||||
}
|
||||
|
||||
int Database::FTSNext(sqlite3_tokenizer_cursor *cursor, const char* *token, int *bytes, int *start_offset, int *end_offset, int *position) {
|
||||
|
||||
UnicodeTokenizerCursor *real_cursor = reinterpret_cast<UnicodeTokenizerCursor*>(cursor);
|
||||
|
||||
QList<Token> tokens = real_cursor->tokens;
|
||||
if (real_cursor->position >= tokens.size()) {
|
||||
return SQLITE_DONE;
|
||||
}
|
||||
|
||||
Token t = tokens[real_cursor->position];
|
||||
QByteArray utf8 = t.token.toUtf8();
|
||||
*token = utf8.constData();
|
||||
*bytes = utf8.size();
|
||||
*start_offset = t.start_offset;
|
||||
*end_offset = t.end_offset;
|
||||
*position = real_cursor->position++;
|
||||
|
||||
real_cursor->current_utf8 = utf8;
|
||||
|
||||
return SQLITE_OK;
|
||||
|
||||
}
|
||||
|
||||
void Database::StaticInit() {
|
||||
|
||||
sFTSTokenizer = new sqlite3_tokenizer_module;
|
||||
sFTSTokenizer->iVersion = 0;
|
||||
sFTSTokenizer->xCreate = &Database::FTSCreate;
|
||||
sFTSTokenizer->xDestroy = &Database::FTSDestroy;
|
||||
sFTSTokenizer->xOpen = &Database::FTSOpen;
|
||||
sFTSTokenizer->xNext = &Database::FTSNext;
|
||||
sFTSTokenizer->xClose = &Database::FTSClose;
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
Database::Database(Application *app, QObject *parent, const QString &database_name) :
|
||||
QObject(parent),
|
||||
app_(app),
|
||||
mutex_(QMutex::Recursive),
|
||||
injected_database_name_(database_name),
|
||||
query_hash_(0),
|
||||
startup_schema_version_(-1) {
|
||||
|
||||
{
|
||||
QMutexLocker l(&sNextConnectionIdMutex);
|
||||
connection_id_ = sNextConnectionId++;
|
||||
}
|
||||
|
||||
directory_ = QDir::toNativeSeparators(Utilities::GetConfigPath(Utilities::Path_Root));
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
Connect();
|
||||
|
||||
}
|
||||
|
||||
QSqlDatabase Database::Connect() {
|
||||
|
||||
QMutexLocker l(&connect_mutex_);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!QFile::exists(directory_)) {
|
||||
QDir dir;
|
||||
if (!dir.mkpath(directory_)) {
|
||||
}
|
||||
}
|
||||
|
||||
const QString connection_id = QString("%1_thread_%2").arg(connection_id_).arg(reinterpret_cast<quint64>(QThread::currentThread()));
|
||||
|
||||
// Try to find an existing connection for this thread
|
||||
QSqlDatabase db = QSqlDatabase::database(connection_id);
|
||||
if (db.isOpen()) {
|
||||
return db;
|
||||
}
|
||||
|
||||
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
|
||||
|
||||
if (!injected_database_name_.isNull())
|
||||
db.setDatabaseName(injected_database_name_);
|
||||
else
|
||||
db.setDatabaseName(directory_ + "/" + kDatabaseFilename);
|
||||
|
||||
if (!db.open()) {
|
||||
app_->AddError("Database: " + db.lastError().text());
|
||||
return db;
|
||||
}
|
||||
|
||||
// Find Sqlite3 functions in the Qt plugin.
|
||||
StaticInit();
|
||||
|
||||
{
|
||||
|
||||
#ifdef SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER
|
||||
// In case sqlite>=3.12 is compiled without -DSQLITE_ENABLE_FTS3_TOKENIZER (generally a good idea due to security reasons) the fts3 support should be enabled explicitly.
|
||||
QVariant v = db.driver()->handle();
|
||||
if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
|
||||
sqlite3 *handle = *static_cast<sqlite3**>(v.data());
|
||||
if (handle) sqlite3_db_config(handle, SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 1, NULL);
|
||||
}
|
||||
#endif
|
||||
QSqlQuery set_fts_tokenizer(db);
|
||||
set_fts_tokenizer.prepare("SELECT fts3_tokenizer(:name, :pointer)");
|
||||
set_fts_tokenizer.bindValue(":name", "unicode");
|
||||
set_fts_tokenizer.bindValue(":pointer", QByteArray(reinterpret_cast<const char*>(&sFTSTokenizer), sizeof(&sFTSTokenizer)));
|
||||
if (!set_fts_tokenizer.exec()) {
|
||||
qLog(Warning) << "Couldn't register FTS3 tokenizer : " << set_fts_tokenizer.lastError();
|
||||
}
|
||||
// Implicit invocation of ~QSqlQuery() when leaving the scope
|
||||
// to release any remaining database locks!
|
||||
}
|
||||
|
||||
if (db.tables().count() == 0) {
|
||||
// Set up initial schema
|
||||
qLog(Info) << "Creating initial database schema";
|
||||
UpdateDatabaseSchema(0, db);
|
||||
}
|
||||
|
||||
// Attach external databases
|
||||
for (const QString &key : attached_databases_.keys()) {
|
||||
QString filename = attached_databases_[key].filename_;
|
||||
|
||||
if (!injected_database_name_.isNull()) filename = injected_database_name_;
|
||||
|
||||
// Attach the db
|
||||
QSqlQuery q(db);
|
||||
q.prepare("ATTACH DATABASE :filename AS :alias");
|
||||
q.bindValue(":filename", filename);
|
||||
q.bindValue(":alias", key);
|
||||
if (!q.exec()) {
|
||||
qFatal("Couldn't attach external database '%s'", key.toLatin1().constData());
|
||||
}
|
||||
}
|
||||
|
||||
if (startup_schema_version_ == -1) {
|
||||
UpdateMainSchema(&db);
|
||||
}
|
||||
|
||||
// We might have to initialise the schema in some attached databases now, if
|
||||
// they were deleted and don't match up with the main schema version.
|
||||
for (const QString &key : attached_databases_.keys()) {
|
||||
if (attached_databases_[key].is_temporary_ && attached_databases_[key].schema_.isEmpty())
|
||||
continue;
|
||||
// Find out if there are any tables in this database
|
||||
QSqlQuery q(db);
|
||||
q.prepare(QString("SELECT ROWID FROM %1.sqlite_master WHERE type='table'").arg(key));
|
||||
if (!q.exec() || !q.next()) {
|
||||
q.finish();
|
||||
ExecSchemaCommandsFromFile(db, attached_databases_[key].schema_, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
|
||||
}
|
||||
|
||||
void Database::UpdateMainSchema(QSqlDatabase *db) {
|
||||
|
||||
// Get the database's schema version
|
||||
int schema_version = 0;
|
||||
{
|
||||
QSqlQuery q("SELECT version FROM schema_version", *db);
|
||||
if (q.next()) schema_version = q.value(0).toInt();
|
||||
// Implicit invocation of ~QSqlQuery() when leaving the scope
|
||||
// to release any remaining database locks!
|
||||
}
|
||||
|
||||
startup_schema_version_ = schema_version;
|
||||
|
||||
if (schema_version > kSchemaVersion) {
|
||||
qLog(Warning) << "The database schema (version" << schema_version << ") is newer than I was expecting";
|
||||
return;
|
||||
}
|
||||
if (schema_version < kSchemaVersion) {
|
||||
// Update the schema
|
||||
for (int v = schema_version + 1; v <= kSchemaVersion; ++v) {
|
||||
UpdateDatabaseSchema(v, *db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Database::RecreateAttachedDb(const QString &database_name) {
|
||||
|
||||
if (!attached_databases_.contains(database_name)) {
|
||||
qLog(Warning) << "Attached database does not exist:" << database_name;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString filename = attached_databases_[database_name].filename_;
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
{
|
||||
QSqlDatabase db(Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("DETACH DATABASE :alias");
|
||||
q.bindValue(":alias", database_name);
|
||||
if (!q.exec()) {
|
||||
qLog(Warning) << "Failed to detach database" << database_name;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QFile::remove(filename)) {
|
||||
qLog(Warning) << "Failed to remove file" << filename;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't just re-attach the database now because it needs to be done for
|
||||
// each thread. Close all the database connections, so each thread will
|
||||
// re-attach it when they next connect.
|
||||
for (const QString &name : QSqlDatabase::connectionNames()) {
|
||||
QSqlDatabase::removeDatabase(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Database::AttachDatabase(const QString &database_name, const AttachedDatabase &database) {
|
||||
attached_databases_[database_name] = database;
|
||||
}
|
||||
|
||||
void Database::AttachDatabaseOnDbConnection(const QString &database_name, const AttachedDatabase &database, QSqlDatabase &db) {
|
||||
|
||||
AttachDatabase(database_name, database);
|
||||
|
||||
// Attach the db
|
||||
QSqlQuery q(db);
|
||||
q.prepare("ATTACH DATABASE :filename AS :alias");
|
||||
q.bindValue(":filename", database.filename_);
|
||||
q.bindValue(":alias", database_name);
|
||||
if (!q.exec()) {
|
||||
qFatal("Couldn't attach external database '%s'", database_name.toLatin1().constData());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Database::DetachDatabase(const QString &database_name) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
{
|
||||
QSqlDatabase db(Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("DETACH DATABASE :alias");
|
||||
q.bindValue(":alias", database_name);
|
||||
if (!q.exec()) {
|
||||
qLog(Warning) << "Failed to detach database" << database_name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
attached_databases_.remove(database_name);
|
||||
|
||||
}
|
||||
|
||||
void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
||||
|
||||
QString filename;
|
||||
if (version == 0) filename = ":/schema/schema.sql";
|
||||
else filename = QString(":/schema/schema-%1.sql").arg(version);
|
||||
|
||||
qLog(Debug) << "Applying database schema update" << version << "from" << filename;
|
||||
ExecSchemaCommandsFromFile(db, filename, version - 1);
|
||||
|
||||
}
|
||||
|
||||
void Database::UrlEncodeFilenameColumn(const QString &table, QSqlDatabase &db) {
|
||||
|
||||
QSqlQuery select(db);
|
||||
select.prepare(QString("SELECT ROWID, filename FROM %1").arg(table));
|
||||
QSqlQuery update(db);
|
||||
update.prepare(QString("UPDATE %1 SET filename=:filename WHERE ROWID=:id").arg(table));
|
||||
select.exec();
|
||||
if (CheckErrors(select)) return;
|
||||
|
||||
while (select.next()) {
|
||||
const int rowid = select.value(0).toInt();
|
||||
const QString filename = select.value(1).toString();
|
||||
|
||||
if (filename.isEmpty() || filename.contains("://")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QUrl url = QUrl::fromLocalFile(filename);
|
||||
|
||||
update.bindValue(":filename", url.toEncoded());
|
||||
update.bindValue(":id", rowid);
|
||||
update.exec();
|
||||
CheckErrors(update);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Database::ExecSchemaCommandsFromFile(QSqlDatabase &db, const QString &filename, int schema_version, bool in_transaction) {
|
||||
|
||||
// Open and read the database schema
|
||||
QFile schema_file(filename);
|
||||
if (!schema_file.open(QIODevice::ReadOnly))
|
||||
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
|
||||
ExecSchemaCommands(db, QString::fromUtf8(schema_file.readAll()), schema_version, in_transaction);
|
||||
|
||||
}
|
||||
|
||||
void Database::ExecSchemaCommands(QSqlDatabase &db, const QString &schema, int schema_version, bool in_transaction) {
|
||||
|
||||
// Run each command
|
||||
const QStringList commands(schema.split(QRegExp("; *\n\n")));
|
||||
|
||||
// We don't want this list to reflect possible DB schema changes
|
||||
// so we initialize it before executing any statements.
|
||||
// If no outer transaction is provided the song tables need to
|
||||
// be queried before beginning an inner transaction! Otherwise
|
||||
// DROP TABLE commands on song tables may fail due to database
|
||||
// locks.
|
||||
const QStringList song_tables(SongsTables(db, schema_version));
|
||||
|
||||
if (!in_transaction) {
|
||||
ScopedTransaction inner_transaction(&db);
|
||||
ExecSongTablesCommands(db, song_tables, commands);
|
||||
inner_transaction.Commit();
|
||||
}
|
||||
else {
|
||||
ExecSongTablesCommands(db, song_tables, commands);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Database::ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_tables, const QStringList &commands) {
|
||||
|
||||
for (const QString &command : commands) {
|
||||
// There are now lots of "songs" tables that need to have the same schema:
|
||||
// songs, magnatune_songs, and device_*_songs. We allow a magic value
|
||||
// in the schema files to update all songs tables at once.
|
||||
if (command.contains(kMagicAllSongsTables)) {
|
||||
for (const QString &table : song_tables) {
|
||||
// Another horrible hack: device songs tables don't have matching _fts
|
||||
// tables, so if this command tries to touch one, ignore it.
|
||||
if (table.startsWith("device_") &&
|
||||
command.contains(QString(kMagicAllSongsTables) + "_fts")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qLog(Info) << "Updating" << table << "for" << kMagicAllSongsTables;
|
||||
QString new_command(command);
|
||||
new_command.replace(kMagicAllSongsTables, table);
|
||||
QSqlQuery query(db.exec(new_command));
|
||||
if (CheckErrors(query))
|
||||
qFatal("Unable to update music collection database");
|
||||
}
|
||||
} else {
|
||||
QSqlQuery query(db.exec(command));
|
||||
if (CheckErrors(query)) qFatal("Unable to update music collection database");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QStringList Database::SongsTables(QSqlDatabase &db, int schema_version) const {
|
||||
|
||||
QStringList ret;
|
||||
|
||||
// look for the tables in the main db
|
||||
for (const QString &table : db.tables()) {
|
||||
if (table == "songs" || table.endsWith("_songs")) ret << table;
|
||||
}
|
||||
|
||||
// look for the tables in attached dbs
|
||||
for (const QString &key : attached_databases_.keys()) {
|
||||
QSqlQuery q(db);
|
||||
q.prepare(QString("SELECT NAME FROM %1.sqlite_master WHERE type='table' AND name='songs' OR name LIKE '%songs'").arg(key));
|
||||
if (q.exec()) {
|
||||
while (q.next()) {
|
||||
QString tab_name = key + "." + q.value(0).toString();
|
||||
ret << tab_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret << "playlist_items";
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
bool Database::CheckErrors(const QSqlQuery &query) {
|
||||
|
||||
QSqlError last_error = query.lastError();
|
||||
if (last_error.isValid()) {
|
||||
qLog(Error) << "db error: " << last_error;
|
||||
qLog(Error) << "faulty query: " << query.lastQuery();
|
||||
qLog(Error) << "bound values: " << query.boundValues();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool Database::IntegrityCheck(QSqlDatabase db) {
|
||||
|
||||
qLog(Debug) << "Starting database integrity check";
|
||||
int task_id = app_->task_manager()->StartTask(tr("Integrity check"));
|
||||
|
||||
bool ok = false;
|
||||
bool error_reported = false;
|
||||
// Ask for 10 error messages at most.
|
||||
QSqlQuery q(QString("PRAGMA integrity_check(10)"), db);
|
||||
while (q.next()) {
|
||||
QString message = q.value(0).toString();
|
||||
|
||||
// If no errors are found, a single row with the value "ok" is returned
|
||||
if (message == "ok") {
|
||||
ok = true;
|
||||
break;
|
||||
} else {
|
||||
if (!error_reported) { app_->AddError(tr("Database corruption detected.")); }
|
||||
app_->AddError("Database: " + message);
|
||||
error_reported = true;
|
||||
}
|
||||
}
|
||||
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
|
||||
return ok;
|
||||
|
||||
}
|
||||
|
||||
void Database::DoBackup() {
|
||||
|
||||
QSqlDatabase db(this->Connect());
|
||||
|
||||
// Before we overwrite anything, make sure the database is not corrupt
|
||||
QMutexLocker l(&mutex_);
|
||||
const bool ok = IntegrityCheck(db);
|
||||
|
||||
if (ok) {
|
||||
BackupFile(db.databaseName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Database::OpenDatabase(const QString &filename, sqlite3 **connection) const {
|
||||
|
||||
int ret = sqlite3_open(filename.toUtf8(), connection);
|
||||
if (ret != 0) {
|
||||
if (*connection) {
|
||||
const char *error_message = sqlite3_errmsg(*connection);
|
||||
qLog(Error) << "Failed to open database for backup:" << filename << error_message;
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Failed to open database for backup:" << filename;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void Database::BackupFile(const QString &filename) {
|
||||
|
||||
qLog(Debug) << "Starting database backup";
|
||||
QString dest_filename = QString("%1.bak").arg(filename);
|
||||
const int task_id = app_->task_manager()->StartTask(tr("Backing up database"));
|
||||
|
||||
sqlite3 *source_connection = nullptr;
|
||||
sqlite3 *dest_connection = nullptr;
|
||||
|
||||
BOOST_SCOPE_EXIT((source_connection)(dest_connection)(task_id)(app_)) {
|
||||
// Harmless to call sqlite3_close() with a nullptr pointer.
|
||||
sqlite3_close(source_connection);
|
||||
sqlite3_close(dest_connection);
|
||||
app_->task_manager()->SetTaskFinished(task_id);
|
||||
}
|
||||
BOOST_SCOPE_EXIT_END
|
||||
|
||||
bool success = OpenDatabase(filename, &source_connection);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
success = OpenDatabase(dest_filename, &dest_connection);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_backup *backup = sqlite3_backup_init(dest_connection, "main", source_connection, "main");
|
||||
if (!backup) {
|
||||
const char *error_message = sqlite3_errmsg(dest_connection);
|
||||
qLog(Error) << "Failed to start database backup:" << error_message;
|
||||
return;
|
||||
}
|
||||
|
||||
int ret = SQLITE_OK;
|
||||
do {
|
||||
ret = sqlite3_backup_step(backup, 16);
|
||||
const int page_count = sqlite3_backup_pagecount(backup);
|
||||
app_->task_manager()->SetTaskProgress(
|
||||
task_id, page_count - sqlite3_backup_remaining(backup), page_count);
|
||||
} while (ret == SQLITE_OK);
|
||||
|
||||
if (ret != SQLITE_DONE) {
|
||||
qLog(Error) << "Database backup failed";
|
||||
}
|
||||
|
||||
sqlite3_backup_finish(backup);
|
||||
|
||||
}
|
||||
|
||||
175
src/core/database.h
Normal file
175
src/core/database.h
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 DATABASE_H
|
||||
#define DATABASE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlError>
|
||||
#include <QStringList>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "gtest/gtest_prod.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct sqlite3_tokenizer;
|
||||
struct sqlite3_tokenizer_cursor;
|
||||
struct sqlite3_tokenizer_module;
|
||||
}
|
||||
|
||||
class Application;
|
||||
|
||||
class Database : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Database(Application *app, QObject *parent = nullptr, const QString &database_name = QString());
|
||||
|
||||
struct AttachedDatabase {
|
||||
AttachedDatabase() {}
|
||||
AttachedDatabase(const QString &filename, const QString &schema, bool is_temporary)
|
||||
: filename_(filename), schema_(schema), is_temporary_(is_temporary) {}
|
||||
|
||||
QString filename_;
|
||||
QString schema_;
|
||||
bool is_temporary_;
|
||||
};
|
||||
|
||||
static const int kSchemaVersion;
|
||||
static const char *kDatabaseFilename;
|
||||
static const char *kMagicAllSongsTables;
|
||||
|
||||
QSqlDatabase Connect();
|
||||
bool CheckErrors(const QSqlQuery &query);
|
||||
QMutex *Mutex() { return &mutex_; }
|
||||
|
||||
void RecreateAttachedDb(const QString &database_name);
|
||||
void ExecSchemaCommands(QSqlDatabase &db, const QString &schema, int schema_version, bool in_transaction = false);
|
||||
|
||||
int startup_schema_version() const { return startup_schema_version_; }
|
||||
int current_schema_version() const { return kSchemaVersion; }
|
||||
|
||||
void AttachDatabase(const QString &database_name, const AttachedDatabase &database);
|
||||
void AttachDatabaseOnDbConnection(const QString &database_name, const AttachedDatabase &database, QSqlDatabase &db);
|
||||
void DetachDatabase(const QString &database_name);
|
||||
|
||||
signals:
|
||||
void Error(const QString &message);
|
||||
|
||||
public slots:
|
||||
void DoBackup();
|
||||
|
||||
private:
|
||||
void UpdateMainSchema(QSqlDatabase *db);
|
||||
|
||||
void ExecSchemaCommandsFromFile(QSqlDatabase &db, const QString &filename, int schema_version, bool in_transaction = false);
|
||||
void ExecSongTablesCommands(QSqlDatabase &db, const QStringList &song_tables, const QStringList &commands);
|
||||
|
||||
void UpdateDatabaseSchema(int version, QSqlDatabase &db);
|
||||
void UrlEncodeFilenameColumn(const QString &table, QSqlDatabase &db);
|
||||
QStringList SongsTables(QSqlDatabase &db, int schema_version) const;
|
||||
bool IntegrityCheck(QSqlDatabase db);
|
||||
void BackupFile(const QString &filename);
|
||||
bool OpenDatabase(const QString &filename, sqlite3 **connection) const;
|
||||
|
||||
Application *app_;
|
||||
|
||||
// Alias -> filename
|
||||
QMap<QString, AttachedDatabase> attached_databases_;
|
||||
|
||||
QString directory_;
|
||||
QMutex connect_mutex_;
|
||||
QMutex mutex_;
|
||||
|
||||
// This ID makes the QSqlDatabase name unique to the object as well as the
|
||||
// thread
|
||||
int connection_id_;
|
||||
|
||||
static QMutex sNextConnectionIdMutex;
|
||||
static int sNextConnectionId;
|
||||
|
||||
// Used by tests
|
||||
QString injected_database_name_;
|
||||
|
||||
uint query_hash_;
|
||||
QStringList query_cache_;
|
||||
|
||||
// This is the schema version of Strawberry's DB from the app's last run.
|
||||
int startup_schema_version_;
|
||||
|
||||
FRIEND_TEST(DatabaseTest, FTSOpenParsesSimpleInput);
|
||||
FRIEND_TEST(DatabaseTest, FTSOpenParsesUTF8Input);
|
||||
FRIEND_TEST(DatabaseTest, FTSOpenParsesMultipleTokens);
|
||||
FRIEND_TEST(DatabaseTest, FTSCursorWorks);
|
||||
FRIEND_TEST(DatabaseTest, FTSOpenLeavesCyrillicQueries);
|
||||
|
||||
// Do static initialisation like loading sqlite functions.
|
||||
static void StaticInit();
|
||||
|
||||
typedef int (*Sqlite3CreateFunc)(sqlite3*, const char*, int, int, void*, void (*)(sqlite3_context*, int, sqlite3_value**), void (*)(sqlite3_context*, int, sqlite3_value**), void (*)(sqlite3_context*));
|
||||
|
||||
static sqlite3_tokenizer_module *sFTSTokenizer;
|
||||
|
||||
static int FTSCreate(int argc, const char *const *argv, sqlite3_tokenizer **tokenizer);
|
||||
static int FTSDestroy(sqlite3_tokenizer *tokenizer);
|
||||
static int FTSOpen(sqlite3_tokenizer *tokenizer, const char *input, int bytes, sqlite3_tokenizer_cursor **cursor);
|
||||
static int FTSClose(sqlite3_tokenizer_cursor *cursor);
|
||||
static int FTSNext(sqlite3_tokenizer_cursor *cursor, const char **token, int *bytes, int *start_offset, int *end_offset, int *position);
|
||||
|
||||
struct Token {
|
||||
Token(const QString &token, int start, int end);
|
||||
QString token;
|
||||
int start_offset;
|
||||
int end_offset;
|
||||
};
|
||||
|
||||
// Based on sqlite3_tokenizer.
|
||||
struct UnicodeTokenizer {
|
||||
const sqlite3_tokenizer_module *pModule;
|
||||
};
|
||||
|
||||
struct UnicodeTokenizerCursor {
|
||||
const sqlite3_tokenizer *pTokenizer;
|
||||
|
||||
QList<Token> tokens;
|
||||
int position;
|
||||
QByteArray current_utf8;
|
||||
};
|
||||
};
|
||||
|
||||
class MemoryDatabase : public Database {
|
||||
public:
|
||||
MemoryDatabase(Application *app, QObject *parent = nullptr)
|
||||
: Database(app, parent, ":memory:") {}
|
||||
~MemoryDatabase() {
|
||||
// Make sure Qt doesn't reuse the same database
|
||||
QSqlDatabase::removeDatabase(Connect().connectionName());
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DATABASE_H
|
||||
|
||||
47
src/core/dbusscreensaver.cpp
Normal file
47
src/core/dbusscreensaver.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "dbusscreensaver.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
|
||||
DBusScreensaver::DBusScreensaver(const QString &service, const QString &path, const QString &interface)
|
||||
: service_(service), path_(path), interface_(interface) {}
|
||||
|
||||
void DBusScreensaver::Inhibit() {
|
||||
|
||||
QDBusInterface gnome_screensaver("org.gnome.ScreenSaver", "/", "org.gnome.ScreenSaver");
|
||||
QDBusReply<quint32> reply = gnome_screensaver.call("Inhibit", QCoreApplication::applicationName(), QObject::tr("Visualizations"));
|
||||
if (reply.isValid()) {
|
||||
cookie_ = reply.value();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DBusScreensaver::Uninhibit() {
|
||||
|
||||
QDBusInterface gnome_screensaver("org.gnome.ScreenSaver", "/", "org.gnome.ScreenSaver");
|
||||
gnome_screensaver.call("UnInhibit", cookie_);
|
||||
|
||||
}
|
||||
45
src/core/dbusscreensaver.h
Normal file
45
src/core/dbusscreensaver.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 DBUSSCREENSAVER_H
|
||||
#define DBUSSCREENSAVER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "screensaver.h"
|
||||
|
||||
class DBusScreensaver : public Screensaver {
|
||||
public:
|
||||
DBusScreensaver(const QString &service, const QString &path, const QString &interface);
|
||||
|
||||
void Inhibit();
|
||||
void Uninhibit();
|
||||
|
||||
private:
|
||||
QString service_;
|
||||
QString path_;
|
||||
QString interface_;
|
||||
|
||||
quint32 cookie_;
|
||||
};
|
||||
|
||||
#endif
|
||||
123
src/core/deletefiles.cpp
Normal file
123
src/core/deletefiles.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 "deletefiles.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
|
||||
#include "musicstorage.h"
|
||||
#include "taskmanager.h"
|
||||
|
||||
const int DeleteFiles::kBatchSize = 50;
|
||||
|
||||
DeleteFiles::DeleteFiles(TaskManager* task_manager, std::shared_ptr<MusicStorage> storage)
|
||||
: thread_(nullptr),
|
||||
task_manager_(task_manager),
|
||||
storage_(storage),
|
||||
started_(false),
|
||||
task_id_(0),
|
||||
progress_(0) {
|
||||
original_thread_ = thread();
|
||||
}
|
||||
|
||||
DeleteFiles::~DeleteFiles() {}
|
||||
|
||||
void DeleteFiles::Start(const SongList &songs) {
|
||||
|
||||
if (thread_) return;
|
||||
|
||||
songs_ = songs;
|
||||
|
||||
task_id_ = task_manager_->StartTask(tr("Deleting files"));
|
||||
task_manager_->SetTaskBlocksCollectionScans(true);
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||
|
||||
moveToThread(thread_);
|
||||
thread_->start();
|
||||
|
||||
}
|
||||
|
||||
void DeleteFiles::Start(const QStringList &filenames) {
|
||||
|
||||
SongList songs;
|
||||
for (const QString &filename : filenames) {
|
||||
Song song;
|
||||
song.set_url(QUrl::fromLocalFile(filename));
|
||||
songs << song;
|
||||
}
|
||||
|
||||
Start(songs);
|
||||
|
||||
}
|
||||
|
||||
void DeleteFiles::ProcessSomeFiles() {
|
||||
|
||||
if (!started_) {
|
||||
storage_->StartDelete();
|
||||
started_ = true;
|
||||
}
|
||||
|
||||
// None left?
|
||||
if (progress_ >= songs_.count()) {
|
||||
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
|
||||
|
||||
storage_->FinishCopy(songs_with_errors_.isEmpty());
|
||||
|
||||
task_manager_->SetTaskFinished(task_id_);
|
||||
|
||||
emit Finished(songs_with_errors_);
|
||||
|
||||
// Move back to the original thread so deleteLater() can get called in
|
||||
// the main thread's event loop
|
||||
moveToThread(original_thread_);
|
||||
deleteLater();
|
||||
|
||||
// Stop this thread
|
||||
thread_->quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// We process files in batches so we can be cancelled part-way through.
|
||||
|
||||
const int n = qMin(songs_.count(), progress_ + kBatchSize);
|
||||
for (; progress_ < n; ++progress_) {
|
||||
task_manager_->SetTaskProgress(task_id_, progress_, songs_.count());
|
||||
|
||||
const Song &song = songs_[progress_];
|
||||
|
||||
MusicStorage::DeleteJob job;
|
||||
job.metadata_ = song;
|
||||
|
||||
if (!storage_->DeleteFromStorage(job)) {
|
||||
songs_with_errors_ << song;
|
||||
}
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
||||
|
||||
}
|
||||
|
||||
70
src/core/deletefiles.h
Normal file
70
src/core/deletefiles.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 DELETEFILES_H
|
||||
#define DELETEFILES_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "song.h"
|
||||
|
||||
class MusicStorage;
|
||||
class TaskManager;
|
||||
|
||||
class DeleteFiles : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeleteFiles(TaskManager *task_manager, std::shared_ptr<MusicStorage> storage);
|
||||
~DeleteFiles();
|
||||
|
||||
static const int kBatchSize;
|
||||
|
||||
void Start(const SongList& songs);
|
||||
void Start(const QStringList& filenames);
|
||||
|
||||
signals:
|
||||
void Finished(const SongList& songs_with_errors);
|
||||
|
||||
private slots:
|
||||
void ProcessSomeFiles();
|
||||
|
||||
private:
|
||||
QThread *thread_;
|
||||
QThread *original_thread_;
|
||||
TaskManager *task_manager_;
|
||||
std::shared_ptr<MusicStorage> storage_;
|
||||
|
||||
SongList songs_;
|
||||
|
||||
bool started_;
|
||||
|
||||
int task_id_;
|
||||
int progress_;
|
||||
|
||||
SongList songs_with_errors_;
|
||||
};
|
||||
|
||||
#endif // DELETEFILES_H
|
||||
|
||||
71
src/core/filesystemmusicstorage.cpp
Normal file
71
src/core/filesystemmusicstorage.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "filesystemmusicstorage.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QUrl>
|
||||
|
||||
FilesystemMusicStorage::FilesystemMusicStorage(const QString &root)
|
||||
: root_(root) {}
|
||||
|
||||
bool FilesystemMusicStorage::CopyToStorage(const CopyJob &job) {
|
||||
|
||||
const QFileInfo src = QFileInfo(job.source_);
|
||||
const QFileInfo dest = QFileInfo(root_ + "/" + job.destination_);
|
||||
|
||||
// Don't do anything if the destination is the same as the source
|
||||
if (src == dest) return true;
|
||||
|
||||
// Create directories as required
|
||||
QDir dir;
|
||||
if (!dir.mkpath(dest.absolutePath())) {
|
||||
qLog(Warning) << "Failed to create directory" << dest.dir().absolutePath();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the destination file if it exists and we want to overwrite
|
||||
if (job.overwrite_ && dest.exists()) QFile::remove(dest.absoluteFilePath());
|
||||
|
||||
// Copy or move
|
||||
if (job.remove_original_)
|
||||
return QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath());
|
||||
else
|
||||
return QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath());
|
||||
|
||||
}
|
||||
|
||||
bool FilesystemMusicStorage::DeleteFromStorage(const DeleteJob &job) {
|
||||
|
||||
QString path = job.metadata_.url().toLocalFile();
|
||||
QFileInfo fileInfo(path);
|
||||
|
||||
if (fileInfo.isDir())
|
||||
return Utilities::RemoveRecursive(path);
|
||||
else
|
||||
return QFile::remove(path);
|
||||
|
||||
}
|
||||
|
||||
43
src/core/filesystemmusicstorage.h
Normal file
43
src/core/filesystemmusicstorage.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 FILESYSTEMMUSICSTORAGE_H
|
||||
#define FILESYSTEMMUSICSTORAGE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "musicstorage.h"
|
||||
|
||||
class FilesystemMusicStorage : public virtual MusicStorage {
|
||||
public:
|
||||
explicit FilesystemMusicStorage(const QString &root);
|
||||
~FilesystemMusicStorage() {}
|
||||
|
||||
QString LocalPath() const { return root_; }
|
||||
|
||||
bool CopyToStorage(const CopyJob &job);
|
||||
bool DeleteFromStorage(const DeleteJob &job);
|
||||
|
||||
private:
|
||||
QString root_;
|
||||
};
|
||||
|
||||
#endif // FILESYSTEMMUSICSTORAGE_H
|
||||
|
||||
46
src/core/filesystemwatcherinterface.cpp
Normal file
46
src/core/filesystemwatcherinterface.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 "filesystemwatcherinterface.h"
|
||||
|
||||
#include "qtfslistener.h"
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include "macfslistener.h"
|
||||
#endif
|
||||
|
||||
FileSystemWatcherInterface::FileSystemWatcherInterface(QObject *parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
FileSystemWatcherInterface *FileSystemWatcherInterface::Create(QObject *parent) {
|
||||
FileSystemWatcherInterface *ret;
|
||||
#ifdef Q_OS_DARWIN
|
||||
ret = new MacFSListener(parent);
|
||||
#else
|
||||
ret = new QtFSListener(parent);
|
||||
#endif
|
||||
|
||||
ret->Init();
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
44
src/core/filesystemwatcherinterface.h
Normal file
44
src/core/filesystemwatcherinterface.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 FILESYSTEMWATCHERINTERFACE_H
|
||||
#define FILESYSTEMWATCHERINTERFACE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class FileSystemWatcherInterface : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FileSystemWatcherInterface(QObject *parent = nullptr);
|
||||
virtual void Init() {}
|
||||
virtual void AddPath(const QString& path) = 0;
|
||||
virtual void RemovePath(const QString& path) = 0;
|
||||
virtual void Clear() = 0;
|
||||
|
||||
static FileSystemWatcherInterface* Create(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void PathChanged(const QString &path);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
183
src/core/flowlayout.cpp
Normal file
183
src/core/flowlayout.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
|
||||
** All rights reserved.
|
||||
** Contact: Nokia Corporation (qt-info@nokia.com)
|
||||
**
|
||||
** This file is part of the examples of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:BSD$
|
||||
** You may use this file under the terms of the BSD license as follows:
|
||||
**
|
||||
** "Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions are
|
||||
** met:
|
||||
** * Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** * Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in
|
||||
** the documentation and/or other materials provided with the
|
||||
** distribution.
|
||||
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
|
||||
** the names of its contributors may be used to endorse or promote
|
||||
** products derived from this software without specific prior written
|
||||
** permission.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "flowlayout.h"
|
||||
//! [1]
|
||||
FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing)
|
||||
: QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing)
|
||||
{
|
||||
setContentsMargins(margin, margin, margin, margin);
|
||||
}
|
||||
|
||||
FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing)
|
||||
: m_hSpace(hSpacing), m_vSpace(vSpacing) {
|
||||
setContentsMargins(margin, margin, margin, margin);
|
||||
}
|
||||
//! [1]
|
||||
|
||||
//! [2]
|
||||
FlowLayout::~FlowLayout() {
|
||||
QLayoutItem* item;
|
||||
while ((item = takeAt(0))) delete item;
|
||||
}
|
||||
//! [2]
|
||||
|
||||
//! [3]
|
||||
void FlowLayout::addItem(QLayoutItem* item) { itemList.append(item); }
|
||||
//! [3]
|
||||
|
||||
//! [4]
|
||||
int FlowLayout::horizontalSpacing() const {
|
||||
if (m_hSpace >= 0) {
|
||||
return m_hSpace;
|
||||
} else {
|
||||
return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
|
||||
}
|
||||
}
|
||||
|
||||
int FlowLayout::verticalSpacing() const {
|
||||
if (m_vSpace >= 0) {
|
||||
return m_vSpace;
|
||||
} else {
|
||||
return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
|
||||
}
|
||||
}
|
||||
//! [4]
|
||||
|
||||
//! [5]
|
||||
int FlowLayout::count() const { return itemList.size(); }
|
||||
|
||||
QLayoutItem* FlowLayout::itemAt(int index) const {
|
||||
return itemList.value(index);
|
||||
}
|
||||
|
||||
QLayoutItem* FlowLayout::takeAt(int index) {
|
||||
if (index >= 0 && index < itemList.size())
|
||||
return itemList.takeAt(index);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
//! [5]
|
||||
|
||||
//! [6]
|
||||
Qt::Orientations FlowLayout::expandingDirections() const { return 0; }
|
||||
//! [6]
|
||||
|
||||
//! [7]
|
||||
bool FlowLayout::hasHeightForWidth() const { return true; }
|
||||
|
||||
int FlowLayout::heightForWidth(int width) const {
|
||||
int height = doLayout(QRect(0, 0, width, 0), true);
|
||||
return height;
|
||||
}
|
||||
//! [7]
|
||||
|
||||
//! [8]
|
||||
void FlowLayout::setGeometry(const QRect& rect) {
|
||||
QLayout::setGeometry(rect);
|
||||
doLayout(rect, false);
|
||||
}
|
||||
|
||||
QSize FlowLayout::sizeHint() const { return minimumSize(); }
|
||||
|
||||
QSize FlowLayout::minimumSize() const {
|
||||
QSize size;
|
||||
for (QLayoutItem* item : itemList)
|
||||
size = size.expandedTo(item->minimumSize());
|
||||
|
||||
size += QSize(2 * margin(), 2 * margin());
|
||||
return size;
|
||||
}
|
||||
//! [8]
|
||||
|
||||
//! [9]
|
||||
int FlowLayout::doLayout(const QRect& rect, bool testOnly) const {
|
||||
int left, top, right, bottom;
|
||||
getContentsMargins(&left, &top, &right, &bottom);
|
||||
QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
|
||||
int x = effectiveRect.x();
|
||||
int y = effectiveRect.y();
|
||||
int lineHeight = 0;
|
||||
//! [9]
|
||||
|
||||
//! [10]
|
||||
for (QLayoutItem* item : itemList) {
|
||||
QWidget* wid = item->widget();
|
||||
int spaceX = horizontalSpacing();
|
||||
if (spaceX == -1)
|
||||
spaceX = wid->style()->layoutSpacing(
|
||||
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
|
||||
int spaceY = verticalSpacing();
|
||||
if (spaceY == -1)
|
||||
spaceY = wid->style()->layoutSpacing(
|
||||
QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
|
||||
//! [10]
|
||||
//! [11]
|
||||
int nextX = x + item->sizeHint().width() + spaceX;
|
||||
if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
|
||||
x = effectiveRect.x();
|
||||
y = y + lineHeight + spaceY;
|
||||
nextX = x + item->sizeHint().width() + spaceX;
|
||||
lineHeight = 0;
|
||||
}
|
||||
|
||||
if (!testOnly) item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
|
||||
|
||||
x = nextX;
|
||||
lineHeight = qMax(lineHeight, item->sizeHint().height());
|
||||
}
|
||||
return y + lineHeight - rect.y() + bottom;
|
||||
}
|
||||
//! [11]
|
||||
//! [12]
|
||||
int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const {
|
||||
QObject* parent = this->parent();
|
||||
if (!parent) {
|
||||
return -1;
|
||||
} else if (parent->isWidgetType()) {
|
||||
QWidget *pw = static_cast<QWidget *>(parent);
|
||||
return pw->style()->pixelMetric(pm, 0, pw);
|
||||
} else {
|
||||
return static_cast<QLayout *>(parent)->spacing();
|
||||
}
|
||||
}
|
||||
//! [12]
|
||||
79
src/core/flowlayout.h
Normal file
79
src/core/flowlayout.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
|
||||
** All rights reserved.
|
||||
** Contact: Nokia Corporation (qt-info@nokia.com)
|
||||
**
|
||||
** This file is part of the examples of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:BSD$
|
||||
** You may use this file under the terms of the BSD license as follows:
|
||||
**
|
||||
** "Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions are
|
||||
** met:
|
||||
** * Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** * Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in
|
||||
** the documentation and/or other materials provided with the
|
||||
** distribution.
|
||||
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
|
||||
** the names of its contributors may be used to endorse or promote
|
||||
** products derived from this software without specific prior written
|
||||
** permission.
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef FLOWLAYOUT_H
|
||||
#define FLOWLAYOUT_H
|
||||
|
||||
#include <QLayout>
|
||||
#include <QRect>
|
||||
#include <QStyle>
|
||||
#include <QWidgetItem>
|
||||
//! [0]
|
||||
class FlowLayout : public QLayout {
|
||||
public:
|
||||
FlowLayout(QWidget* parent, int margin = -1, int hSpacing = -1,
|
||||
int vSpacing = -1);
|
||||
FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
|
||||
~FlowLayout();
|
||||
|
||||
void addItem(QLayoutItem *item);
|
||||
int horizontalSpacing() const;
|
||||
int verticalSpacing() const;
|
||||
Qt::Orientations expandingDirections() const;
|
||||
bool hasHeightForWidth() const;
|
||||
int heightForWidth(int) const;
|
||||
int count() const;
|
||||
QLayoutItem *itemAt(int index) const;
|
||||
QSize minimumSize() const;
|
||||
void setGeometry(const QRect &rect);
|
||||
QSize sizeHint() const;
|
||||
QLayoutItem *takeAt(int index);
|
||||
|
||||
private:
|
||||
int doLayout(const QRect &rect, bool testOnly) const;
|
||||
int smartSpacing(QStyle::PixelMetric pm) const;
|
||||
|
||||
QList<QLayoutItem *> itemList;
|
||||
int m_hSpace;
|
||||
int m_vSpace;
|
||||
};
|
||||
//! [0]
|
||||
|
||||
#endif
|
||||
80
src/core/iconloader.cpp
Normal file
80
src/core/iconloader.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2013, Jonas Kvinge <jonas@strawbs.net>
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <QFile>
|
||||
#include <QDir>
|
||||
#include <QtDebug>
|
||||
#include <QSettings>
|
||||
|
||||
#include "iconloader.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/appearance.h"
|
||||
|
||||
QList<int> IconLoader::sizes_;
|
||||
QString IconDefault(":/icons/64x64/strawberry.png");
|
||||
|
||||
void IconLoader::Init() {
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
sizes_.clear();
|
||||
sizes_ << 22 << 32 << 48 << 64;
|
||||
|
||||
if (!QFile::exists(IconDefault)) {
|
||||
qLog(Error) << "Default icon does not exist" << IconDefault;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QIcon IconLoader::Load(const QString &name) {
|
||||
|
||||
QIcon ret;
|
||||
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__ << name;
|
||||
|
||||
if (name.isEmpty()) {
|
||||
qLog(Warning) << "Icon name is empty!";
|
||||
ret.addFile(IconDefault, QSize(64, 64));
|
||||
return ret;
|
||||
}
|
||||
|
||||
const QString path(":icons/%1x%2/%3.png");
|
||||
for (int size : sizes_) {
|
||||
QString filename(path.arg(size).arg(size).arg(name));
|
||||
if (QFile::exists(filename)) ret.addFile(filename, QSize(size, size));
|
||||
}
|
||||
|
||||
// Load icon from system theme only if it hasn't been found
|
||||
if (ret.isNull()) {
|
||||
ret = QIcon::fromTheme(name);
|
||||
if (!ret.isNull()) return ret;
|
||||
qLog(Warning) << "Couldn't load icon" << name;
|
||||
}
|
||||
|
||||
if (ret.isNull()) {
|
||||
ret.addFile(IconDefault, QSize(64, 64));
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
38
src/core/iconloader.h
Normal file
38
src/core/iconloader.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 ICONLOADER_H
|
||||
#define ICONLOADER_H
|
||||
|
||||
#include <QIcon>
|
||||
|
||||
class IconLoader {
|
||||
public:
|
||||
|
||||
static void Init();
|
||||
static QIcon Load(const QString &name);
|
||||
|
||||
private:
|
||||
IconLoader() {}
|
||||
|
||||
static QList<int> sizes_;
|
||||
};
|
||||
|
||||
#endif // ICONLOADER_H
|
||||
33
src/core/mac_delegate.h
Normal file
33
src/core/mac_delegate.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#import <AppKit/NSApplication.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "macglobalshortcutbackend.h"
|
||||
|
||||
class PlatformInterface;
|
||||
@class SPMediaKeyTap;
|
||||
|
||||
@interface AppDelegate : NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate> {
|
||||
PlatformInterface* application_handler_;
|
||||
NSMenu* dock_menu_;
|
||||
MacGlobalShortcutBackend* shortcut_handler_;
|
||||
SPMediaKeyTap* key_tap_;
|
||||
|
||||
}
|
||||
|
||||
- (id) initWithHandler: (PlatformInterface*)handler;
|
||||
|
||||
// NSApplicationDelegate
|
||||
- (BOOL) applicationShouldHandleReopen: (NSApplication*)app hasVisibleWindows:(BOOL)flag;
|
||||
- (NSMenu*) applicationDockMenu: (NSApplication*)sender;
|
||||
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification;
|
||||
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender;
|
||||
|
||||
// NSUserNotificationCenterDelegate
|
||||
- (BOOL) userNotificationCenter: (id)center
|
||||
shouldPresentNotification: (id)notification;
|
||||
|
||||
- (void) setDockMenu: (NSMenu*)menu;
|
||||
- (MacGlobalShortcutBackend*) shortcut_handler;
|
||||
- (void) setShortcutHandler: (MacGlobalShortcutBackend*)backend;
|
||||
- (void) mediaKeyTap: (SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event;
|
||||
@end
|
||||
37
src/core/mac_startup.h
Normal file
37
src/core/mac_startup.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef MAC_STARTUP_H
|
||||
#define MAC_STARTUP_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
|
||||
class MacGlobalShortcutBackend;
|
||||
class QObject;
|
||||
class QWidget;
|
||||
|
||||
class PlatformInterface {
|
||||
public:
|
||||
// Called when the application should show itself.
|
||||
virtual void Activate() = 0;
|
||||
virtual bool LoadUrl(const QString& url) = 0;
|
||||
|
||||
virtual ~PlatformInterface() {}
|
||||
};
|
||||
|
||||
namespace mac {
|
||||
|
||||
void MacMain();
|
||||
void SetShortcutHandler(MacGlobalShortcutBackend* handler);
|
||||
void SetApplicationHandler(PlatformInterface* handler);
|
||||
void CheckForUpdates();
|
||||
|
||||
QString GetBundlePath();
|
||||
QString GetResourcesPath();
|
||||
QString GetApplicationSupportPath();
|
||||
QString GetMusicDirectory();
|
||||
|
||||
void EnableFullScreen(const QWidget& main_window);
|
||||
|
||||
} // namespace mac
|
||||
|
||||
#endif
|
||||
457
src/core/mac_startup.mm
Normal file
457
src/core/mac_startup.mm
Normal file
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#import <AppKit/NSApplication.h>
|
||||
#import <AppKit/NSEvent.h>
|
||||
#import <AppKit/NSGraphics.h>
|
||||
#import <AppKit/NSNibDeclarations.h>
|
||||
#import <AppKit/NSViewController.h>
|
||||
|
||||
#import <Foundation/NSBundle.h>
|
||||
#import <Foundation/NSError.h>
|
||||
#import <Foundation/NSFileManager.h>
|
||||
#import <Foundation/NSPathUtilities.h>
|
||||
#import <Foundation/NSProcessInfo.h>
|
||||
#import <Foundation/NSThread.h>
|
||||
#import <Foundation/NSTimer.h>
|
||||
#import <Foundation/NSURL.h>
|
||||
|
||||
#import <IOKit/hidsystem/ev_keymap.h>
|
||||
|
||||
#import <Kernel/AvailabilityMacros.h>
|
||||
|
||||
#import <QuartzCore/CALayer.h>
|
||||
|
||||
#import "3rdparty/SPMediaKeyTap/SPMediaKeyTap.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "globalshortcuts.h"
|
||||
#include "mac_delegate.h"
|
||||
#include "mac_startup.h"
|
||||
#include "mac_utilities.h"
|
||||
#include "macglobalshortcutbackend.h"
|
||||
#include "utilities.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_cftyperef.h"
|
||||
#include "core/scoped_nsautorelease_pool.h"
|
||||
|
||||
#ifdef HAVE_SPARKLE
|
||||
#import <Sparkle/SUUpdater.h>
|
||||
#endif
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <QWidget>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
QDebug operator<<(QDebug dbg, NSObject* object) {
|
||||
QString ns_format = [[NSString stringWithFormat:@"%@", object] UTF8String];
|
||||
dbg.nospace() << ns_format;
|
||||
return dbg.space();
|
||||
}
|
||||
|
||||
// Capture global media keys on Mac (Cocoa only!)
|
||||
// See: http://www.rogueamoeba.com/utm/2007/09/29/apple-keyboard-media-key-event-handling/
|
||||
|
||||
@interface MacApplication : NSApplication {
|
||||
PlatformInterface* application_handler_;
|
||||
AppDelegate* delegate_;
|
||||
// shortcut_handler_ only used to temporarily save it
|
||||
// AppDelegate does all the heavy-shortcut-lifting
|
||||
MacGlobalShortcutBackend* shortcut_handler_;
|
||||
}
|
||||
|
||||
- (MacGlobalShortcutBackend*)shortcut_handler;
|
||||
- (void)SetShortcutHandler:(MacGlobalShortcutBackend*)handler;
|
||||
|
||||
- (PlatformInterface*)application_handler;
|
||||
- (void)SetApplicationHandler:(PlatformInterface*)handler;
|
||||
|
||||
@end
|
||||
|
||||
#ifdef HAVE_BREAKPAD
|
||||
static bool BreakpadCallback(int, int, mach_port_t, void*) { return true; }
|
||||
|
||||
static BreakpadRef InitBreakpad() {
|
||||
ScopedNSAutoreleasePool pool;
|
||||
BreakpadRef breakpad = nil;
|
||||
NSDictionary* plist = [[NSBundle mainBundle] infoDictionary];
|
||||
if (plist) {
|
||||
breakpad = BreakpadCreate(plist);
|
||||
BreakpadSetFilterCallback(breakpad, &BreakpadCallback, nullptr);
|
||||
}
|
||||
[pool release];
|
||||
return breakpad;
|
||||
}
|
||||
#endif // HAVE_BREAKPAD
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
- (id)init {
|
||||
if ((self = [super init])) {
|
||||
application_handler_ = nil;
|
||||
shortcut_handler_ = nil;
|
||||
dock_menu_ = nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithHandler:(PlatformInterface*)handler {
|
||||
application_handler_ = handler;
|
||||
|
||||
#ifdef HAVE_BREAKPAD
|
||||
breakpad_ = InitBreakpad();
|
||||
#endif
|
||||
|
||||
// Register defaults for the whitelist of apps that want to use media keys
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers], kMediaKeyUsingBundleIdentifiersDefaultsKey,
|
||||
nil]];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL) applicationShouldHandleReopen: (NSApplication*)app hasVisibleWindows:(BOOL)flag {
|
||||
if (application_handler_) {
|
||||
application_handler_->Activate();
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setDockMenu:(NSMenu*)menu {
|
||||
dock_menu_ = menu;
|
||||
}
|
||||
|
||||
- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
|
||||
return dock_menu_;
|
||||
}
|
||||
|
||||
- (void)setShortcutHandler:(MacGlobalShortcutBackend*)backend {
|
||||
shortcut_handler_ = backend;
|
||||
}
|
||||
|
||||
- (MacGlobalShortcutBackend*)shortcut_handler {
|
||||
return shortcut_handler_;
|
||||
}
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification {
|
||||
key_tap_ = [[SPMediaKeyTap alloc] initWithDelegate:self];
|
||||
if ([SPMediaKeyTap usesGlobalMediaKeyTap] &&
|
||||
![[NSProcessInfo processInfo]
|
||||
isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){
|
||||
.majorVersion = 10,
|
||||
.minorVersion = 12,
|
||||
.patchVersion = 0}]) {
|
||||
[key_tap_ startWatchingMediaKeys];
|
||||
}
|
||||
else {
|
||||
qLog(Warning) << "Media key monitoring disabled";
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)application:(NSApplication*)app openFile:(NSString*)filename {
|
||||
qLog(Debug) << "Wants to open:" << [filename UTF8String];
|
||||
|
||||
if (application_handler_->LoadUrl(QString::fromUtf8([filename UTF8String]))) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)application:(NSApplication*)app openFiles:(NSArray*)filenames {
|
||||
qLog(Debug) << "Wants to open:" << filenames;
|
||||
[filenames enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL* stop) {
|
||||
[self application:app openFile:(NSString*)object];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) mediaKeyTap: (SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event {
|
||||
NSAssert([event type] == NSSystemDefined && [event subtype] == SPSystemDefinedEventMediaKeys, @"Unexpected NSEvent in mediaKeyTap:receivedMediaKeyEvent:");
|
||||
|
||||
int key_code = (([event data1] & 0xFFFF0000) >> 16);
|
||||
int key_flags = ([event data1] & 0x0000FFFF);
|
||||
BOOL key_is_released = (((key_flags & 0xFF00) >> 8)) == 0xB;
|
||||
// not used. keep just in case
|
||||
// int key_repeat = (key_flags & 0x1);
|
||||
|
||||
if (!shortcut_handler_) {
|
||||
return;
|
||||
}
|
||||
if (key_is_released) {
|
||||
shortcut_handler_->MacMediaKeyPressed(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
- (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*) sender {
|
||||
#ifdef HAVE_BREAKPAD
|
||||
BreakpadRelease(breakpad_);
|
||||
#endif
|
||||
return NSTerminateNow;
|
||||
}
|
||||
|
||||
- (BOOL) userNotificationCenter: (id)center shouldPresentNotification: (id)notification {
|
||||
// Always show notifications, even if Strawberry is in the foreground.
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MacApplication
|
||||
|
||||
- (id)init {
|
||||
if ((self = [super init])) {
|
||||
[self SetShortcutHandler:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MacGlobalShortcutBackend*)shortcut_handler {
|
||||
// should be the same as delegate_'s shortcut handler
|
||||
return shortcut_handler_;
|
||||
}
|
||||
|
||||
- (void)SetShortcutHandler:(MacGlobalShortcutBackend*)handler {
|
||||
shortcut_handler_ = handler;
|
||||
if (delegate_) [delegate_ setShortcutHandler:handler];
|
||||
}
|
||||
|
||||
- (PlatformInterface*)application_handler {
|
||||
return application_handler_;
|
||||
}
|
||||
|
||||
- (void)SetApplicationHandler:(PlatformInterface*)handler {
|
||||
delegate_ = [[AppDelegate alloc] initWithHandler:handler];
|
||||
// App-shortcut-handler set before delegate is set.
|
||||
// this makes sure the delegate's shortcut_handler is set
|
||||
[delegate_ setShortcutHandler:shortcut_handler_];
|
||||
[self setDelegate:delegate_];
|
||||
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter]
|
||||
setDelegate:delegate_];
|
||||
}
|
||||
|
||||
- (void)sendEvent:(NSEvent*)event {
|
||||
// If event tap is not installed, handle events that reach the app instead
|
||||
BOOL shouldHandleMediaKeyEventLocally = ![SPMediaKeyTap usesGlobalMediaKeyTap];
|
||||
|
||||
if(shouldHandleMediaKeyEventLocally && [event type] == NSSystemDefined && [event subtype] == SPSystemDefinedEventMediaKeys) {
|
||||
[(id)[self delegate] mediaKeyTap:nil receivedMediaKeyEvent:event];
|
||||
}
|
||||
|
||||
[super sendEvent:event];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace mac {
|
||||
|
||||
void MacMain() {
|
||||
ScopedNSAutoreleasePool pool;
|
||||
// Creates and sets the magic global variable so QApplication will find it.
|
||||
[MacApplication sharedApplication];
|
||||
#ifdef HAVE_SPARKLE
|
||||
// Creates and sets the magic global variable for Sparkle.
|
||||
[[SUUpdater sharedUpdater] setDelegate:NSApp];
|
||||
#endif
|
||||
}
|
||||
|
||||
void SetShortcutHandler(MacGlobalShortcutBackend* handler) {
|
||||
[NSApp SetShortcutHandler:handler];
|
||||
}
|
||||
|
||||
void SetApplicationHandler(PlatformInterface* handler) {
|
||||
[NSApp SetApplicationHandler:handler];
|
||||
}
|
||||
|
||||
void CheckForUpdates() {
|
||||
#ifdef HAVE_SPARKLE
|
||||
[[SUUpdater sharedUpdater] checkForUpdates:NSApp];
|
||||
#endif
|
||||
}
|
||||
|
||||
QString GetBundlePath() {
|
||||
ScopedCFTypeRef<CFURLRef> app_url(CFBundleCopyBundleURL(CFBundleGetMainBundle()));
|
||||
ScopedCFTypeRef<CFStringRef> mac_path(CFURLCopyFileSystemPath(app_url.get(), kCFURLPOSIXPathStyle));
|
||||
const char* path = CFStringGetCStringPtr(mac_path.get(), CFStringGetSystemEncoding());
|
||||
QString bundle_path = QString::fromUtf8(path);
|
||||
return bundle_path;
|
||||
}
|
||||
|
||||
QString GetResourcesPath() {
|
||||
QString bundle_path = GetBundlePath();
|
||||
return bundle_path + "/Contents/Resources";
|
||||
}
|
||||
|
||||
QString GetApplicationSupportPath() {
|
||||
ScopedNSAutoreleasePool pool;
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(
|
||||
NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
||||
QString ret;
|
||||
if ([paths count] > 0) {
|
||||
NSString* user_path = [paths objectAtIndex:0];
|
||||
ret = QString::fromUtf8([user_path UTF8String]);
|
||||
} else {
|
||||
ret = "~/Collection/Application Support";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString GetMusicDirectory() {
|
||||
ScopedNSAutoreleasePool pool;
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSMusicDirectory,
|
||||
NSUserDomainMask, YES);
|
||||
QString ret;
|
||||
if ([paths count] > 0) {
|
||||
NSString* user_path = [paths objectAtIndex:0];
|
||||
ret = QString::fromUtf8([user_path UTF8String]);
|
||||
} else {
|
||||
ret = "~/Music";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int MapFunctionKey(int keycode) {
|
||||
switch (keycode) {
|
||||
// Function keys
|
||||
case NSInsertFunctionKey: return Qt::Key_Insert;
|
||||
case NSDeleteFunctionKey: return Qt::Key_Delete;
|
||||
case NSPauseFunctionKey: return Qt::Key_Pause;
|
||||
case NSPrintFunctionKey: return Qt::Key_Print;
|
||||
case NSSysReqFunctionKey: return Qt::Key_SysReq;
|
||||
case NSHomeFunctionKey: return Qt::Key_Home;
|
||||
case NSEndFunctionKey: return Qt::Key_End;
|
||||
case NSLeftArrowFunctionKey: return Qt::Key_Left;
|
||||
case NSUpArrowFunctionKey: return Qt::Key_Up;
|
||||
case NSRightArrowFunctionKey: return Qt::Key_Right;
|
||||
case NSDownArrowFunctionKey: return Qt::Key_Down;
|
||||
case NSPageUpFunctionKey: return Qt::Key_PageUp;
|
||||
case NSPageDownFunctionKey: return Qt::Key_PageDown;
|
||||
case NSScrollLockFunctionKey: return Qt::Key_ScrollLock;
|
||||
case NSF1FunctionKey: return Qt::Key_F1;
|
||||
case NSF2FunctionKey: return Qt::Key_F2;
|
||||
case NSF3FunctionKey: return Qt::Key_F3;
|
||||
case NSF4FunctionKey: return Qt::Key_F4;
|
||||
case NSF5FunctionKey: return Qt::Key_F5;
|
||||
case NSF6FunctionKey: return Qt::Key_F6;
|
||||
case NSF7FunctionKey: return Qt::Key_F7;
|
||||
case NSF8FunctionKey: return Qt::Key_F8;
|
||||
case NSF9FunctionKey: return Qt::Key_F9;
|
||||
case NSF10FunctionKey: return Qt::Key_F10;
|
||||
case NSF11FunctionKey: return Qt::Key_F11;
|
||||
case NSF12FunctionKey: return Qt::Key_F12;
|
||||
case NSF13FunctionKey: return Qt::Key_F13;
|
||||
case NSF14FunctionKey: return Qt::Key_F14;
|
||||
case NSF15FunctionKey: return Qt::Key_F15;
|
||||
case NSF16FunctionKey: return Qt::Key_F16;
|
||||
case NSF17FunctionKey: return Qt::Key_F17;
|
||||
case NSF18FunctionKey: return Qt::Key_F18;
|
||||
case NSF19FunctionKey: return Qt::Key_F19;
|
||||
case NSF20FunctionKey: return Qt::Key_F20;
|
||||
case NSF21FunctionKey: return Qt::Key_F21;
|
||||
case NSF22FunctionKey: return Qt::Key_F22;
|
||||
case NSF23FunctionKey: return Qt::Key_F23;
|
||||
case NSF24FunctionKey: return Qt::Key_F24;
|
||||
case NSF25FunctionKey: return Qt::Key_F25;
|
||||
case NSF26FunctionKey: return Qt::Key_F26;
|
||||
case NSF27FunctionKey: return Qt::Key_F27;
|
||||
case NSF28FunctionKey: return Qt::Key_F28;
|
||||
case NSF29FunctionKey: return Qt::Key_F29;
|
||||
case NSF30FunctionKey: return Qt::Key_F30;
|
||||
case NSF31FunctionKey: return Qt::Key_F31;
|
||||
case NSF32FunctionKey: return Qt::Key_F32;
|
||||
case NSF33FunctionKey: return Qt::Key_F33;
|
||||
case NSF34FunctionKey: return Qt::Key_F34;
|
||||
case NSF35FunctionKey: return Qt::Key_F35;
|
||||
case NSMenuFunctionKey: return Qt::Key_Menu;
|
||||
case NSHelpFunctionKey: return Qt::Key_Help;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
QKeySequence KeySequenceFromNSEvent(NSEvent* event) {
|
||||
NSString* str = [event charactersIgnoringModifiers];
|
||||
NSString* upper = [str uppercaseString];
|
||||
const char* chars = [upper UTF8String];
|
||||
NSUInteger modifiers = [event modifierFlags];
|
||||
int key = 0;
|
||||
unsigned char c = chars[0];
|
||||
switch (c) {
|
||||
case 0x1b: key = Qt::Key_Escape; break;
|
||||
case 0x09: key = Qt::Key_Tab; break;
|
||||
case 0x0d: key = Qt::Key_Return; break;
|
||||
case 0x08: key = Qt::Key_Backspace; break;
|
||||
case 0x03: key = Qt::Key_Enter; break;
|
||||
}
|
||||
|
||||
if (key == 0) {
|
||||
if (c >= 0x20 && c <= 0x7e) { // ASCII from space to ~
|
||||
key = c;
|
||||
} else {
|
||||
key = MapFunctionKey([event keyCode]);
|
||||
if (key == 0) {
|
||||
return QKeySequence();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers & NSShiftKeyMask) {
|
||||
key += Qt::SHIFT;
|
||||
}
|
||||
if (modifiers & NSControlKeyMask) {
|
||||
key += Qt::META;
|
||||
}
|
||||
if (modifiers & NSAlternateKeyMask) {
|
||||
key += Qt::ALT;
|
||||
}
|
||||
if (modifiers & NSCommandKeyMask) {
|
||||
key += Qt::CTRL;
|
||||
}
|
||||
|
||||
return QKeySequence(key);
|
||||
}
|
||||
|
||||
void DumpDictionary(CFDictionaryRef dict) {
|
||||
NSDictionary* d = (NSDictionary*)dict;
|
||||
NSLog(@"%@", d);
|
||||
}
|
||||
|
||||
// NSWindowCollectionBehaviorFullScreenPrimary
|
||||
static const NSUInteger kFullScreenPrimary = 1 << 7;
|
||||
|
||||
void EnableFullScreen(const QWidget& main_window) {
|
||||
|
||||
NSView* view = reinterpret_cast<NSView*>(main_window.winId());
|
||||
NSWindow* window = [view window];
|
||||
[window setCollectionBehavior:kFullScreenPrimary];
|
||||
}
|
||||
|
||||
float GetDevicePixelRatio(QWidget* widget) {
|
||||
NSView* view = reinterpret_cast<NSView*>(widget->winId());
|
||||
return [[view window] backingScaleFactor];
|
||||
}
|
||||
|
||||
} // namespace mac
|
||||
|
||||
38
src/core/mac_utilities.h
Normal file
38
src/core/mac_utilities.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2011, 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 <QKeySequence>
|
||||
|
||||
#include <CoreFoundation/CFDictionary.h>
|
||||
|
||||
#ifdef __OBJC__
|
||||
@class NSEvent;
|
||||
#else
|
||||
class NSEvent;
|
||||
#endif
|
||||
|
||||
namespace mac {
|
||||
|
||||
QKeySequence KeySequenceFromNSEvent(NSEvent* event);
|
||||
void DumpDictionary(CFDictionaryRef dict);
|
||||
float GetDevicePixelRatio(QWidget* widget);
|
||||
}
|
||||
63
src/core/macfslistener.h
Normal file
63
src/core/macfslistener.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 MACFSLISTENER_H
|
||||
#define MACFSLISTENER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <CoreServices/CoreServices.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
|
||||
#include "filesystemwatcherinterface.h"
|
||||
|
||||
class MacFSListener : public FileSystemWatcherInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MacFSListener(QObject *parent = nullptr);
|
||||
void Init();
|
||||
void AddPath(const QString &path);
|
||||
void RemovePath(const QString &path);
|
||||
void Clear();
|
||||
|
||||
signals:
|
||||
void PathChanged(const QString &path);
|
||||
|
||||
private slots:
|
||||
void UpdateStream();
|
||||
|
||||
private:
|
||||
void UpdateStreamAsync();
|
||||
|
||||
static void EventStreamCallback(ConstFSEventStreamRef stream, void *user_data, size_t num_events, void *event_paths, const FSEventStreamEventFlags event_flags[], const FSEventStreamEventId event_ids[]);
|
||||
|
||||
CFRunLoopRef run_loop_;
|
||||
FSEventStreamRef stream_;
|
||||
|
||||
QSet<QString> paths_;
|
||||
QTimer update_timer_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
112
src/core/macfslistener.mm
Normal file
112
src/core/macfslistener.mm
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "macfslistener.h"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <CoreFoundation/CFArray.h>
|
||||
#include <Foundation/NSArray.h>
|
||||
#include <Foundation/NSString.h>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_nsobject.h"
|
||||
|
||||
MacFSListener::MacFSListener(QObject* parent)
|
||||
: FileSystemWatcherInterface(parent), run_loop_(nullptr), stream_(nullptr) {
|
||||
update_timer_.setSingleShot(true);
|
||||
update_timer_.setInterval(2000);
|
||||
connect(&update_timer_, SIGNAL(timeout()), SLOT(UpdateStream()));
|
||||
}
|
||||
|
||||
void MacFSListener::Init() { run_loop_ = CFRunLoopGetCurrent(); }
|
||||
|
||||
void MacFSListener::EventStreamCallback(
|
||||
ConstFSEventStreamRef stream,
|
||||
void* user_data,
|
||||
size_t num_events,
|
||||
void* event_paths,
|
||||
const FSEventStreamEventFlags event_flags[],
|
||||
const FSEventStreamEventId event_ids[]) {
|
||||
MacFSListener* me = reinterpret_cast<MacFSListener*>(user_data);
|
||||
char** paths = reinterpret_cast<char**>(event_paths);
|
||||
for (int i = 0; i < num_events; ++i) {
|
||||
QString path = QString::fromUtf8(paths[i]);
|
||||
qLog(Debug) << "Something changed at:" << path;
|
||||
while (path.endsWith('/')) {
|
||||
path.chop(1);
|
||||
}
|
||||
emit me->PathChanged(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MacFSListener::AddPath(const QString& path) {
|
||||
Q_ASSERT(run_loop_);
|
||||
paths_.insert(path);
|
||||
UpdateStreamAsync();
|
||||
}
|
||||
|
||||
void MacFSListener::RemovePath(const QString& path) {
|
||||
Q_ASSERT(run_loop_);
|
||||
paths_.remove(path);
|
||||
UpdateStreamAsync();
|
||||
}
|
||||
|
||||
void MacFSListener::Clear() {
|
||||
paths_.clear();
|
||||
UpdateStreamAsync();
|
||||
}
|
||||
|
||||
void MacFSListener::UpdateStreamAsync() { update_timer_.start(); }
|
||||
|
||||
void MacFSListener::UpdateStream() {
|
||||
if (stream_) {
|
||||
FSEventStreamStop(stream_);
|
||||
FSEventStreamInvalidate(stream_);
|
||||
FSEventStreamRelease(stream_);
|
||||
stream_ = nullptr;
|
||||
}
|
||||
|
||||
if (paths_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
scoped_nsobject<NSMutableArray> array([[NSMutableArray alloc] init]);
|
||||
|
||||
for (const QString& path : paths_) {
|
||||
scoped_nsobject<NSString> string(
|
||||
[[NSString alloc] initWithUTF8String:path.toUtf8().constData()]);
|
||||
[array addObject:string.get()];
|
||||
}
|
||||
|
||||
FSEventStreamContext context;
|
||||
memset(&context, 0, sizeof(context));
|
||||
context.info = this;
|
||||
CFAbsoluteTime latency = 1.0;
|
||||
|
||||
stream_ = FSEventStreamCreate(nullptr, &EventStreamCallback, &context, // Copied
|
||||
reinterpret_cast<CFArrayRef>(array.get()),
|
||||
kFSEventStreamEventIdSinceNow, latency,
|
||||
kFSEventStreamCreateFlagNone);
|
||||
|
||||
FSEventStreamScheduleWithRunLoop(stream_, run_loop_, kCFRunLoopDefaultMode);
|
||||
FSEventStreamStart(stream_);
|
||||
}
|
||||
|
||||
46
src/core/macscreensaver.cpp
Normal file
46
src/core/macscreensaver.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "macscreensaver.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/utilities.h"
|
||||
|
||||
// kIOPMAssertionTypePreventUserIdleDisplaySleep from Lion.
|
||||
#define kLionDisplayAssertion CFSTR("PreventUserIdleDisplaySleep")
|
||||
|
||||
MacScreensaver::MacScreensaver() : assertion_id_(0) {}
|
||||
|
||||
void MacScreensaver::Inhibit() {
|
||||
CFStringRef assertion_type = (Utilities::GetMacVersion() >= 7) ? kLionDisplayAssertion : kIOPMAssertionTypeNoDisplaySleep;
|
||||
|
||||
IOPMAssertionCreateWithName(
|
||||
assertion_type,
|
||||
kIOPMAssertionLevelOn,
|
||||
CFSTR("Showing full-screen Strawberry visualisations"),
|
||||
&assertion_id_);
|
||||
}
|
||||
|
||||
void MacScreensaver::Uninhibit() {
|
||||
IOPMAssertionRelease(assertion_id_);
|
||||
}
|
||||
41
src/core/macscreensaver.h
Normal file
41
src/core/macscreensaver.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MACSCREENSAVER_H
|
||||
#define MACSCREENSAVER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "screensaver.h"
|
||||
|
||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
||||
|
||||
class MacScreensaver : public Screensaver {
|
||||
public:
|
||||
MacScreensaver();
|
||||
|
||||
void Inhibit();
|
||||
void Uninhibit();
|
||||
|
||||
private:
|
||||
IOPMAssertionID assertion_id_;
|
||||
};
|
||||
|
||||
#endif
|
||||
61
src/core/macsystemtrayicon.h
Normal file
61
src/core/macsystemtrayicon.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
*Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MACSYSTEMTRAYICON_H
|
||||
#define MACSYSTEMTRAYICON_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "systemtrayicon.h"
|
||||
|
||||
class MacSystemTrayIconPrivate;
|
||||
|
||||
class MacSystemTrayIcon : public SystemTrayIcon {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacSystemTrayIcon(QObject *parent = nullptr);
|
||||
~MacSystemTrayIcon();
|
||||
|
||||
void SetupMenu(QAction *previous, QAction *play, QAction *stop, QAction *stop_after, QAction *next, QAction *mute, QAction *quit);
|
||||
|
||||
void SetNowPlaying(const Song& song, const QString& image_path);
|
||||
void ClearNowPlaying();
|
||||
|
||||
private:
|
||||
void SetupMenuItem(QAction *action);
|
||||
|
||||
private slots:
|
||||
void ActionChanged();
|
||||
|
||||
protected:
|
||||
// SystemTrayIcon
|
||||
void UpdateIcon();
|
||||
|
||||
private:
|
||||
QPixmap orange_icon_;
|
||||
QPixmap grey_icon_;
|
||||
std::unique_ptr<MacSystemTrayIconPrivate> p_;
|
||||
Q_DISABLE_COPY(MacSystemTrayIcon);
|
||||
};
|
||||
|
||||
#endif // MACSYSTEMTRAYICON_H
|
||||
210
src/core/macsystemtrayicon.mm
Normal file
210
src/core/macsystemtrayicon.mm
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "macsystemtrayicon.h"
|
||||
|
||||
#include "core/mac_delegate.h"
|
||||
#include "core/song.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QIcon>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#include <AppKit/NSMenu.h>
|
||||
#include <AppKit/NSMenuItem.h>
|
||||
|
||||
@interface Target :NSObject {
|
||||
QAction* action_;
|
||||
}
|
||||
- (id) initWithQAction: (QAction*)action;
|
||||
- (void) clicked;
|
||||
@end
|
||||
|
||||
@implementation Target // <NSMenuValidation>
|
||||
- (id) init {
|
||||
return [super init];
|
||||
}
|
||||
|
||||
- (id) initWithQAction: (QAction*)action {
|
||||
action_ = action;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL) validateMenuItem: (NSMenuItem*)menuItem {
|
||||
// This is called when the menu is shown.
|
||||
return action_->isEnabled();
|
||||
}
|
||||
|
||||
- (void) clicked {
|
||||
action_->trigger();
|
||||
}
|
||||
@end
|
||||
|
||||
class MacSystemTrayIconPrivate {
|
||||
public:
|
||||
MacSystemTrayIconPrivate() {
|
||||
dock_menu_ = [[NSMenu alloc] initWithTitle:@"DockMenu"];
|
||||
|
||||
QString title = QT_TR_NOOP("Now Playing");
|
||||
NSString* t = [[NSString alloc] initWithUTF8String:title.toUtf8().constData()];
|
||||
now_playing_ = [[NSMenuItem alloc]
|
||||
initWithTitle:t
|
||||
action:nullptr
|
||||
keyEquivalent:@""];
|
||||
|
||||
now_playing_artist_ = [[NSMenuItem alloc]
|
||||
initWithTitle:@"Nothing to see here"
|
||||
action:nullptr
|
||||
keyEquivalent:@""];
|
||||
|
||||
now_playing_title_ = [[NSMenuItem alloc]
|
||||
initWithTitle:@"Nothing to see here"
|
||||
action:nullptr
|
||||
keyEquivalent:@""];
|
||||
|
||||
[dock_menu_ insertItem:now_playing_title_ atIndex:0];
|
||||
[dock_menu_ insertItem:now_playing_artist_ atIndex:0];
|
||||
[dock_menu_ insertItem:now_playing_ atIndex:0];
|
||||
|
||||
// Don't look now.
|
||||
// This must be called after our custom NSApplicationDelegate has been set.
|
||||
[(AppDelegate*)([NSApp delegate]) setDockMenu:dock_menu_];
|
||||
|
||||
ClearPlaying();
|
||||
}
|
||||
|
||||
void AddMenuItem(QAction* action) {
|
||||
// Strip accelarators from name.
|
||||
QString text = action->text().remove("&");
|
||||
NSString* title = [[NSString alloc] initWithUTF8String: text.toUtf8().constData()];
|
||||
// Create an object that can receive user clicks and pass them on to the QAction.
|
||||
Target* target = [[Target alloc] initWithQAction:action];
|
||||
NSMenuItem* item = [[[NSMenuItem alloc]
|
||||
initWithTitle:title
|
||||
action:@selector(clicked)
|
||||
keyEquivalent:@""] autorelease];
|
||||
[item setEnabled:action->isEnabled()];
|
||||
[item setTarget:target];
|
||||
[dock_menu_ addItem:item];
|
||||
actions_[action] = item;
|
||||
}
|
||||
|
||||
void ActionChanged(QAction* action) {
|
||||
NSMenuItem* item = actions_[action];
|
||||
NSString* title = [[NSString alloc] initWithUTF8String: action->text().toUtf8().constData()];
|
||||
[item setTitle:title];
|
||||
}
|
||||
|
||||
void AddSeparator() {
|
||||
NSMenuItem* separator = [NSMenuItem separatorItem];
|
||||
[dock_menu_ addItem:separator];
|
||||
}
|
||||
|
||||
void ShowPlaying(const QString& artist, const QString& title) {
|
||||
ClearPlaying(); // Makes sure the order is consistent.
|
||||
[now_playing_artist_ setTitle:
|
||||
[[NSString alloc] initWithUTF8String: artist.toUtf8().constData()]];
|
||||
[now_playing_title_ setTitle:
|
||||
[[NSString alloc] initWithUTF8String: title.toUtf8().constData()]];
|
||||
title.isEmpty() ? HideItem(now_playing_title_) : ShowItem(now_playing_title_);
|
||||
artist.isEmpty() ? HideItem(now_playing_artist_) : ShowItem(now_playing_artist_);
|
||||
artist.isEmpty() && title.isEmpty() ? HideItem(now_playing_) : ShowItem(now_playing_);
|
||||
}
|
||||
|
||||
void ClearPlaying() {
|
||||
// Hiding doesn't seem to work in the dock menu.
|
||||
HideItem(now_playing_);
|
||||
HideItem(now_playing_artist_);
|
||||
HideItem(now_playing_title_);
|
||||
}
|
||||
|
||||
private:
|
||||
void HideItem(NSMenuItem* item) {
|
||||
if ([dock_menu_ indexOfItem:item] != -1) {
|
||||
[dock_menu_ removeItem:item];
|
||||
}
|
||||
}
|
||||
|
||||
void ShowItem(NSMenuItem* item, int index = 0) {
|
||||
if ([dock_menu_ indexOfItem:item] == -1) {
|
||||
[dock_menu_ insertItem:item atIndex:index];
|
||||
}
|
||||
}
|
||||
|
||||
QMap<QAction*, NSMenuItem*> actions_;
|
||||
|
||||
NSMenu* dock_menu_;
|
||||
NSMenuItem* now_playing_;
|
||||
NSMenuItem* now_playing_artist_;
|
||||
NSMenuItem* now_playing_title_;
|
||||
|
||||
Q_DISABLE_COPY(MacSystemTrayIconPrivate);
|
||||
};
|
||||
|
||||
MacSystemTrayIcon::MacSystemTrayIcon(QObject* parent)
|
||||
: SystemTrayIcon(parent),
|
||||
orange_icon_(QPixmap(":/icons/64x64/strawberry.png").scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)),
|
||||
grey_icon_(QPixmap(":icon_large_grey.png").scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)) {
|
||||
QApplication::setWindowIcon(orange_icon_);
|
||||
}
|
||||
|
||||
MacSystemTrayIcon::~MacSystemTrayIcon() {
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::SetupMenu(QAction* previous, QAction* play, QAction* stop, QAction* stop_after, QAction* next, QAction* mute, QAction* quit) {
|
||||
|
||||
p_.reset(new MacSystemTrayIconPrivate());
|
||||
SetupMenuItem(previous);
|
||||
SetupMenuItem(play);
|
||||
SetupMenuItem(stop);
|
||||
SetupMenuItem(stop_after);
|
||||
SetupMenuItem(next);
|
||||
p_->AddSeparator();
|
||||
SetupMenuItem(mute);
|
||||
p_->AddSeparator();
|
||||
Q_UNUSED(quit); // Mac already has a Quit item.
|
||||
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::SetupMenuItem(QAction* action) {
|
||||
p_->AddMenuItem(action);
|
||||
connect(action, SIGNAL(changed()), SLOT(ActionChanged()));
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::UpdateIcon() {
|
||||
QApplication::setWindowIcon(CreateIcon(orange_icon_, grey_icon_));
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::ActionChanged() {
|
||||
QAction* action = qobject_cast<QAction*>(sender());
|
||||
p_->ActionChanged(action);
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::ClearPlaying() {
|
||||
p_->ClearPlaying();
|
||||
}
|
||||
|
||||
void MacSystemTrayIcon::SetPlaying(const Song& song, const QString& image_path) {
|
||||
p_->ShowPlaying(song.artist(), song.PrettyTitle());
|
||||
}
|
||||
305
src/core/main.cpp
Normal file
305
src/core/main.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <memory>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <glib-object.h>
|
||||
#include <glib.h>
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
#include <gst/gst.h>
|
||||
#include <gst/pbutils/pbutils.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
#include <unistd.h>
|
||||
#endif // Q_OS_UNIX
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
#include "core/mpris.h"
|
||||
#include "core/mpris2.h"
|
||||
#include <QDBusArgument>
|
||||
#include <QImage>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include <sys/resource.h>
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#include <windows.h>
|
||||
#include <iostream>
|
||||
#include <qtsparkle/Updater>
|
||||
#endif // Q_OS_WIN32
|
||||
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QFont>
|
||||
#include <QLibraryInfo>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QSslSocket>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QSysInfo>
|
||||
#include <QTextCodec>
|
||||
#include <QTranslator>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/mainwindow.h"
|
||||
#include "core/commandlineoptions.h"
|
||||
#include "core/database.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mac_startup.h"
|
||||
#include "core/metatypes.h"
|
||||
#include "core/network.h"
|
||||
#include "core/networkproxyfactory.h"
|
||||
#include "core/song.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/systemtrayicon.h"
|
||||
#include "core/scangiomodulepath.h"
|
||||
#include "engine/enginebase.h"
|
||||
#ifdef HAVE_GSTREAMER
|
||||
#include "engine/gstengine.h"
|
||||
#endif
|
||||
#include "version.h"
|
||||
#include "widgets/osd.h"
|
||||
#if 0
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
#include "covermanager/lastfmcoverprovider.h"
|
||||
#endif
|
||||
#include "covermanager/amazoncoverprovider.h"
|
||||
#include "covermanager/coverproviders.h"
|
||||
#include "covermanager/musicbrainzcoverprovider.h"
|
||||
#include "covermanager/discogscoverprovider.h"
|
||||
#endif
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#include "qtsingleapplication.h"
|
||||
#include "qtsinglecoreapplication.h"
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image);
|
||||
const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &image);
|
||||
#endif
|
||||
|
||||
// Load sqlite plugin on windows and mac.
|
||||
#include <QtPlugin>
|
||||
Q_IMPORT_PLUGIN(QSQLiteDriverPlugin)
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Do Mac specific startup to get media keys working.
|
||||
// This must go before QApplication initialisation.
|
||||
mac::MacMain();
|
||||
|
||||
if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) {
|
||||
// Work around 10.9 issue.
|
||||
// https://bugreports.qt-project.org/browse/QTBUG-32789
|
||||
QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||
QCoreApplication::setApplicationName("Strawberry");
|
||||
QCoreApplication::setOrganizationName("Strawberry");
|
||||
#else
|
||||
QCoreApplication::setApplicationName("strawberry");
|
||||
QCoreApplication::setOrganizationName("strawberry");
|
||||
#endif
|
||||
QCoreApplication::setApplicationVersion(STRAWBERRY_VERSION_DISPLAY);
|
||||
QCoreApplication::setOrganizationDomain("strawbs.org");
|
||||
|
||||
// This makes us show up nicely in gnome-volume-control
|
||||
#if !GLIB_CHECK_VERSION(2, 36, 0)
|
||||
g_type_init(); // Deprecated in glib 2.36.0
|
||||
#endif
|
||||
g_set_application_name(QCoreApplication::applicationName().toLocal8Bit());
|
||||
|
||||
RegisterMetaTypes();
|
||||
|
||||
// Initialise logging. Log levels are set after the commandline options are
|
||||
// parsed below.
|
||||
logging::Init();
|
||||
g_log_set_default_handler(reinterpret_cast<GLogFunc>(&logging::GLog), nullptr);
|
||||
|
||||
CommandlineOptions options(argc, argv);
|
||||
|
||||
{
|
||||
// Only start a core application now so we can check if there's another
|
||||
// Strawberry running without needing an X server.
|
||||
// This MUST be done before parsing the commandline options so QTextCodec
|
||||
// gets the right system locale for filenames.
|
||||
QtSingleCoreApplication a(argc, argv);
|
||||
Utilities::CheckPortable();
|
||||
//crash_reporting.SetApplicationPath(a.applicationFilePath());
|
||||
|
||||
// Parse commandline options - need to do this before starting the
|
||||
// full QApplication so it works without an X server
|
||||
if (!options.Parse()) return 1;
|
||||
logging::SetLevels(options.log_levels());
|
||||
|
||||
if (a.isRunning()) {
|
||||
if (options.is_empty()) {
|
||||
qLog(Info) << "Strawberry is already running - activating existing window";
|
||||
}
|
||||
if (a.sendMessage(options.Serialize(), 5000)) {
|
||||
return 0;
|
||||
}
|
||||
// Couldn't send the message so start anyway
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Must happen after QCoreApplication::setOrganizationName().
|
||||
setenv("XDG_CONFIG_HOME", Utilities::GetConfigPath(Utilities::Path_Root).toLocal8Bit().constData(), 1);
|
||||
#endif
|
||||
|
||||
// Output the version, so when people attach log output to bug reports they
|
||||
// don't have to tell us which version they're using.
|
||||
qLog(Info) << "Strawberry" << STRAWBERRY_VERSION_DISPLAY;
|
||||
|
||||
// Seed the random number generators.
|
||||
time_t t = time(nullptr);
|
||||
srand(t);
|
||||
qsrand(t);
|
||||
|
||||
Utilities::IncreaseFDLimit();
|
||||
|
||||
QtSingleApplication a(argc, argv);
|
||||
|
||||
// A bug in Qt means the wheel_scroll_lines setting gets ignored and replaced
|
||||
// with the default value of 3 in QApplicationPrivate::initialize.
|
||||
{
|
||||
QSettings qt_settings(QSettings::UserScope, "Trolltech");
|
||||
qt_settings.beginGroup("Qt");
|
||||
QApplication::setWheelScrollLines(qt_settings.value("wheelScrollLines", QApplication::wheelScrollLines()).toInt());
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
QCoreApplication::setCollectionPaths(
|
||||
QStringList() << QCoreApplication::applicationDirPath() + "/../PlugIns");
|
||||
#endif
|
||||
|
||||
a.setQuitOnLastWindowClosed(false);
|
||||
|
||||
// Do this check again because another instance might have started by now
|
||||
if (a.isRunning() && a.sendMessage(options.Serialize(), 5000)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef Q_OS_DARWIN
|
||||
// Gnome on Ubuntu has menu icons disabled by default. I think that's a bad
|
||||
// idea, and makes some menus in Strawberry look confusing.
|
||||
QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, false);
|
||||
#else
|
||||
QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, true);
|
||||
// Fixes focus issue with NSSearchField, see QTBUG-11401
|
||||
QCoreApplication::setAttribute(Qt::AA_NativeWindows, true);
|
||||
#endif
|
||||
|
||||
// Set the permissions on the config file on Unix - it can contain passwords
|
||||
// for internet services so it's important that other users can't read it.
|
||||
// On Windows these are stored in the registry instead.
|
||||
#ifdef Q_OS_UNIX
|
||||
{
|
||||
QSettings s;
|
||||
|
||||
// Create the file if it doesn't exist already
|
||||
if (!QFile::exists(s.fileName())) {
|
||||
QFile file(s.fileName());
|
||||
file.open(QIODevice::WriteOnly);
|
||||
}
|
||||
|
||||
// Set -rw-------
|
||||
QFile::setPermissions(s.fileName(), QFile::ReadOwner | QFile::WriteOwner);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Resources
|
||||
Q_INIT_RESOURCE(data);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Set the language for qtsparkle
|
||||
qtsparkle::LoadTranslations(language);
|
||||
#endif
|
||||
|
||||
// Icons
|
||||
IconLoader::Init();
|
||||
|
||||
// This is a nasty hack to ensure that everything in libprotobuf is
|
||||
// initialised in the main thread. It fixes issue 3265 but nobody knows why.
|
||||
// Don't remove this unless you can reproduce the error that it fixes.
|
||||
//ParseAProto();
|
||||
//QtConcurrent::run(&ParseAProto);
|
||||
|
||||
Application app;
|
||||
|
||||
// Network proxy
|
||||
QNetworkProxyFactory::setApplicationProxyFactory(NetworkProxyFactory::Instance());
|
||||
|
||||
#if 0
|
||||
//#ifdef HAVE_LIBLASTFM
|
||||
app.cover_providers()->AddProvider(new LastFmCoverProvider);
|
||||
app.cover_providers()->AddProvider(new AmazonCoverProvider);
|
||||
app.cover_providers()->AddProvider(new DiscogsCoverProvider);
|
||||
app.cover_providers()->AddProvider(new MusicbrainzCoverProvider);
|
||||
#endif
|
||||
|
||||
// Create the tray icon and OSD
|
||||
std::unique_ptr<SystemTrayIcon> tray_icon(SystemTrayIcon::CreateSystemTrayIcon());
|
||||
OSD osd(tray_icon.get(), &app);
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
mpris::Mpris mpris(&app);
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
gst_init(nullptr, nullptr);
|
||||
gst_pb_utils_init();
|
||||
#endif
|
||||
|
||||
// Window
|
||||
MainWindow w(&app, tray_icon.get(), &osd, options);
|
||||
#ifdef Q_OS_DARWIN
|
||||
mac::EnableFullScreen(w);
|
||||
#endif // Q_OS_DARWIN
|
||||
#ifdef HAVE_GIO
|
||||
ScanGIOModulePath();
|
||||
#endif
|
||||
#ifdef HAVE_DBUS
|
||||
QObject::connect(&mpris, SIGNAL(RaiseMainWindow()), &w, SLOT(Raise()));
|
||||
#endif
|
||||
QObject::connect(&a, SIGNAL(messageReceived(QString)), &w, SLOT(CommandlineOptionsReceived(QString)));
|
||||
|
||||
int ret = a.exec();
|
||||
|
||||
return ret;
|
||||
}
|
||||
2300
src/core/mainwindow.cpp
Normal file
2300
src/core/mainwindow.cpp
Normal file
File diff suppressed because it is too large
Load Diff
353
src/core/mainwindow.h
Normal file
353
src/core/mainwindow.h
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QSettings>
|
||||
#include <QSystemTrayIcon>
|
||||
|
||||
#include "core/lazy.h"
|
||||
#include "core/mac_startup.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "engine/engine_fwd.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#ifdef HAVE_GSTREAMER
|
||||
#include "dialogs/organisedialog.h"
|
||||
#endif
|
||||
#include "settings/settingsdialog.h"
|
||||
|
||||
class StatusView;
|
||||
class About;
|
||||
class AlbumCoverManager;
|
||||
class Appearance;
|
||||
class Application;
|
||||
class ArtistInfoView;
|
||||
class CommandlineOptions;
|
||||
class CoverProviders;
|
||||
class Database;
|
||||
class DeviceManager;
|
||||
class DeviceView;
|
||||
class DeviceViewContainer;
|
||||
class EditTagDialog;
|
||||
class Equalizer;
|
||||
class ErrorDialog;
|
||||
class FileView;
|
||||
class GlobalShortcuts;
|
||||
class GroupByDialog;
|
||||
class Collection;
|
||||
class CollectionViewContainer;
|
||||
class MimeData;
|
||||
class MultiLoadingIndicator;
|
||||
class OSD;
|
||||
class Player;
|
||||
class PlaylistBackend;
|
||||
class PlaylistListContainer;
|
||||
class PlaylistManager;
|
||||
class QueueManager;
|
||||
class Song;
|
||||
class SystemTrayIcon;
|
||||
class TagFetcher;
|
||||
class TaskManager;
|
||||
class TrackSelectionDialog;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
class TranscodeDialog;
|
||||
#endif
|
||||
class Windows7ThumbBar;
|
||||
class Ui_MainWindow;
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(Application *app, SystemTrayIcon* tray_icon, OSD* osd, const CommandlineOptions& options, QWidget* parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const char *kAllFilesFilterSpec;
|
||||
|
||||
// Don't change the values
|
||||
enum StartupBehaviour {
|
||||
Startup_Remember = 1,
|
||||
Startup_AlwaysShow = 2,
|
||||
Startup_AlwaysHide = 3,
|
||||
};
|
||||
|
||||
// Don't change the values
|
||||
enum AddBehaviour {
|
||||
AddBehaviour_Append = 1,
|
||||
AddBehaviour_Enqueue = 2,
|
||||
AddBehaviour_Load = 3,
|
||||
AddBehaviour_OpenInNew = 4
|
||||
};
|
||||
|
||||
// Don't change the values
|
||||
enum PlayBehaviour {
|
||||
PlayBehaviour_Never = 1,
|
||||
PlayBehaviour_IfStopped = 2,
|
||||
PlayBehaviour_Always = 3,
|
||||
};
|
||||
|
||||
// Don't change the values
|
||||
enum PlaylistAddBehaviour {
|
||||
PlaylistAddBehaviour_Play = 1,
|
||||
PlaylistAddBehaviour_Enqueue = 2,
|
||||
};
|
||||
|
||||
void SetHiddenInTray(bool hidden);
|
||||
void CommandlineOptionsReceived(const CommandlineOptions& options);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event);
|
||||
void resizeEvent(QResizeEvent* event);
|
||||
void closeEvent(QCloseEvent* event);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
bool winEvent(MSG* message, long* result);
|
||||
#endif
|
||||
|
||||
// PlatformInterface
|
||||
void Activate();
|
||||
bool LoadUrl(const QString& url);
|
||||
|
||||
signals:
|
||||
// Signals that stop playing after track was toggled.
|
||||
void StopAfterToggled(bool stop);
|
||||
|
||||
void IntroPointReached();
|
||||
|
||||
private slots:
|
||||
void FilePathChanged(const QString& path);
|
||||
|
||||
void MediaStopped();
|
||||
void MediaPaused();
|
||||
void MediaPlaying();
|
||||
void TrackSkipped(PlaylistItemPtr item);
|
||||
void ForceShowOSD(const Song& song, const bool toggle);
|
||||
|
||||
void PlaylistRightClick(const QPoint& global_pos, const QModelIndex& index);
|
||||
void PlaylistCurrentChanged(const QModelIndex& current);
|
||||
void PlaylistViewSelectionModelChanged();
|
||||
void PlaylistPlay();
|
||||
void PlaylistStopAfter();
|
||||
void PlaylistQueue();
|
||||
void PlaylistSkip();
|
||||
void PlaylistRemoveCurrent();
|
||||
void PlaylistEditFinished(const QModelIndex& index);
|
||||
void EditTracks();
|
||||
void EditTagDialogAccepted();
|
||||
void RenumberTracks();
|
||||
void SelectionSetValue();
|
||||
void EditValue();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void AutoCompleteTags();
|
||||
void AutoCompleteTagsAccepted();
|
||||
#endif
|
||||
void PlaylistUndoRedoChanged(QAction* undo, QAction* redo);
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void AddFilesToTranscoder();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void PlaylistCopyToCollection();
|
||||
void PlaylistMoveToCollection();
|
||||
void PlaylistCopyToDevice();
|
||||
void PlaylistOrganiseSelected(bool copy);
|
||||
#endif
|
||||
//void PlaylistDelete();
|
||||
void PlaylistOpenInBrowser();
|
||||
void ShowInCollection();
|
||||
|
||||
void ChangeCollectionQueryMode(QAction* action);
|
||||
|
||||
void PlayIndex(const QModelIndex& index);
|
||||
void PlaylistDoubleClick(const QModelIndex& index);
|
||||
void StopAfterCurrent();
|
||||
|
||||
void SongChanged(const Song& song);
|
||||
void VolumeChanged(int volume);
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void CopyFilesToCollection(const QList<QUrl>& urls);
|
||||
void MoveFilesToCollection(const QList<QUrl>& urls);
|
||||
void CopyFilesToDevice(const QList<QUrl>& urls);
|
||||
#endif
|
||||
void EditFileTags(const QList<QUrl>& urls);
|
||||
|
||||
void AddToPlaylist(QMimeData* data);
|
||||
void AddToPlaylist(QAction* action);
|
||||
|
||||
void VolumeWheelEvent(int delta);
|
||||
void ToggleShowHide();
|
||||
|
||||
void Seeked(qlonglong microseconds);
|
||||
void UpdateTrackPosition();
|
||||
void UpdateTrackSliderPosition();
|
||||
|
||||
void TaskCountChanged(int count);
|
||||
|
||||
void ShowCollectionConfig();
|
||||
void ReloadSettings();
|
||||
void ReloadAllSettings();
|
||||
void RefreshStyleSheet();
|
||||
void SetHiddenInTray() { SetHiddenInTray(true); }
|
||||
|
||||
void AddFile();
|
||||
void AddFolder();
|
||||
void AddCDTracks();
|
||||
|
||||
void CommandlineOptionsReceived(const QString& string_options);
|
||||
|
||||
void CheckForUpdates();
|
||||
|
||||
void PlayingWidgetPositionChanged();
|
||||
|
||||
void SongSaveComplete(TagReaderReply *reply, const QPersistentModelIndex& index);
|
||||
|
||||
void ShowCoverManager();
|
||||
|
||||
void ShowAboutDialog();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void ShowTranscodeDialog();
|
||||
#endif
|
||||
void ShowErrorDialog(const QString& message);
|
||||
void ShowQueueManager();
|
||||
void EnsureSettingsDialogCreated();
|
||||
void EnsureEditTagDialogCreated();
|
||||
SettingsDialog* CreateSettingsDialog();
|
||||
EditTagDialog* CreateEditTagDialog();
|
||||
void OpenSettingsDialog();
|
||||
void OpenSettingsDialogAtPage(SettingsDialog::Page page);
|
||||
|
||||
void TabSwitched();
|
||||
void SaveGeometry();
|
||||
void SavePlaybackStatus();
|
||||
void LoadPlaybackStatus();
|
||||
void ResumePlayback();
|
||||
|
||||
void Raise();
|
||||
|
||||
void Exit();
|
||||
|
||||
void HandleNotificationPreview(OSD::Behaviour type, QString line1, QString line2);
|
||||
void FocusCollectionTab();
|
||||
|
||||
void ShowConsole();
|
||||
|
||||
private:
|
||||
void ConnectStatusView(StatusView *statusview);
|
||||
|
||||
void ApplyAddBehaviour(AddBehaviour b, MimeData *data) const;
|
||||
void ApplyPlayBehaviour(PlayBehaviour b, MimeData *data) const;
|
||||
|
||||
void CheckFullRescanRevisions();
|
||||
|
||||
// creates the icon by painting the full one depending on the current position
|
||||
QPixmap CreateOverlayedIcon(int position, int scrobble_point);
|
||||
|
||||
private:
|
||||
Ui_MainWindow *ui_;
|
||||
Windows7ThumbBar *thumbbar_;
|
||||
|
||||
Application *app_;
|
||||
SystemTrayIcon * tray_icon_;
|
||||
OSD* osd_;
|
||||
Lazy<EditTagDialog> edit_tag_dialog_;
|
||||
Lazy<About> about_dialog_;
|
||||
|
||||
GlobalShortcuts* global_shortcuts_;
|
||||
|
||||
CollectionViewContainer *collection_view_;
|
||||
StatusView *status_view_;
|
||||
FileView *file_view_;
|
||||
PlaylistListContainer *playlist_list_;
|
||||
DeviceViewContainer *device_view_container_;
|
||||
DeviceView *device_view_;
|
||||
|
||||
Lazy<SettingsDialog> settings_dialog_;
|
||||
Lazy<AlbumCoverManager> cover_manager_;
|
||||
std::unique_ptr<Equalizer> equalizer_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
Lazy<TranscodeDialog> transcode_dialog_;
|
||||
#endif
|
||||
Lazy<ErrorDialog> error_dialog_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
Lazy<OrganiseDialog> organise_dialog_;
|
||||
#endif
|
||||
Lazy<QueueManager> queue_manager_;
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
std::unique_ptr<TagFetcher> tag_fetcher_;
|
||||
#endif
|
||||
std::unique_ptr<TrackSelectionDialog> track_selection_dialog_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
PlaylistItemList autocomplete_tag_items_;
|
||||
#endif
|
||||
|
||||
QAction *collection_show_all_;
|
||||
QAction *collection_show_duplicates_;
|
||||
QAction *collection_show_untagged_;
|
||||
|
||||
QMenu *playlist_menu_;
|
||||
QAction *playlist_play_pause_;
|
||||
QAction *playlist_stop_after_;
|
||||
QAction *playlist_undoredo_;
|
||||
//QAction *playlist_organise_;
|
||||
QAction *playlist_show_in_collection_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
QAction *playlist_copy_to_collection_;
|
||||
QAction *playlist_move_to_collection_;
|
||||
QAction *playlist_copy_to_device_;
|
||||
//QAction *playlist_delete_;
|
||||
#endif
|
||||
QAction *playlist_open_in_browser_;
|
||||
QAction *playlist_queue_;
|
||||
QAction *playlist_skip_;
|
||||
QAction *playlist_add_to_another_;
|
||||
QList<QAction*> playlistitem_actions_;
|
||||
QAction *playlistitem_actions_separator_;
|
||||
QModelIndex playlist_menu_index_;
|
||||
|
||||
QSortFilterProxyModel *collection_sort_model_;
|
||||
|
||||
QTimer *track_position_timer_;
|
||||
QTimer *track_slider_timer_;
|
||||
QSettings settings_;
|
||||
|
||||
bool was_maximized_;
|
||||
int saved_playback_position_;
|
||||
Engine::State saved_playback_state_;
|
||||
AddBehaviour doubleclick_addmode_;
|
||||
PlayBehaviour doubleclick_playmode_;
|
||||
PlaylistAddBehaviour doubleclick_playlist_addmode_;
|
||||
PlayBehaviour menu_playmode_;
|
||||
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
784
src/core/mainwindow.ui
Normal file
784
src/core/mainwindow.ui
Normal file
@@ -0,0 +1,784 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1131</width>
|
||||
<height>685</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Strawberry Music Player</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralWidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="sidebar_layout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="FancyTabWidget" name="tabs" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PlayingWidget" name="now_playing" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="playlist_layout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="PlaylistContainer" name="playlist" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<addaction name="action_edit_track"/>
|
||||
<addaction name="action_edit_value"/>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="Line" name="line_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="player_controls_container" native="true">
|
||||
<layout class="QVBoxLayout" name="player_controls_layout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="player_controls">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QToolButton" name="back_button">
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="pause_play_button">
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="stop_button">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::MenuButtonPopup</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="forward_button">
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>22</width>
|
||||
<height>22</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_buttons">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AnalyzerContainer" name="analyzer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>36</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_volume">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Amarok::VolumeSlider" name="volume">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="status_bar" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="status_bar_layout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="Line" name="status_bar_line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="status_bar_internal" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="status_bar_stack">
|
||||
<widget class="MultiLoadingIndicator" name="multi_loading_indicator"/>
|
||||
<widget class="QWidget" name="playlist_summary_page">
|
||||
<layout class="QVBoxLayout" name="playlist_summary_layout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="playlist_summary">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PlaylistSequence" name="playlist_sequence" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="TrackSlider" name="track_slider" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1131</width>
|
||||
<height>24</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menu_music">
|
||||
<property name="title">
|
||||
<string>&Music</string>
|
||||
</property>
|
||||
<addaction name="action_open_file"/>
|
||||
<addaction name="action_open_cd"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_previous_track"/>
|
||||
<addaction name="action_play_pause"/>
|
||||
<addaction name="action_stop"/>
|
||||
<addaction name="action_next_track"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_mute"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_quit"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_playlist">
|
||||
<property name="title">
|
||||
<string>&Playlist</string>
|
||||
</property>
|
||||
<addaction name="action_add_file"/>
|
||||
<addaction name="action_add_folder"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_shuffle_mode"/>
|
||||
<addaction name="action_repeat_mode"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_new_playlist"/>
|
||||
<addaction name="action_save_playlist"/>
|
||||
<addaction name="action_load_playlist"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_jump"/>
|
||||
<addaction name="action_clear_playlist"/>
|
||||
<addaction name="action_shuffle"/>
|
||||
<addaction name="action_remove_duplicates"/>
|
||||
<addaction name="action_remove_unavailable"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_help">
|
||||
<property name="title">
|
||||
<string>&Help</string>
|
||||
</property>
|
||||
<addaction name="action_about_strawberry"/>
|
||||
<addaction name="action_about_qt"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_tools">
|
||||
<property name="title">
|
||||
<string>&Tools</string>
|
||||
</property>
|
||||
<addaction name="action_cover_manager"/>
|
||||
<addaction name="action_queue_manager"/>
|
||||
<addaction name="action_equalizer"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_update_collection"/>
|
||||
<addaction name="action_full_collection_scan"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="action_settings"/>
|
||||
<addaction name="action_console"/>
|
||||
</widget>
|
||||
<addaction name="menu_music"/>
|
||||
<addaction name="menu_playlist"/>
|
||||
<addaction name="menu_tools"/>
|
||||
<addaction name="menu_help"/>
|
||||
</widget>
|
||||
<action name="action_previous_track">
|
||||
<property name="text">
|
||||
<string>&Previous track</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F5</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_play_pause">
|
||||
<property name="text">
|
||||
<string>P&lay</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F6</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_stop">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Stop</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F7</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_next_track">
|
||||
<property name="text">
|
||||
<string>&Next track</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F8</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_quit">
|
||||
<property name="text">
|
||||
<string>&Quit</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::QuitRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_stop_after_this_track">
|
||||
<property name="text">
|
||||
<string>Stop after this track</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Alt+V</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_clear_playlist">
|
||||
<property name="text">
|
||||
<string>&Clear playlist</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Clear playlist</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+K</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_edit_track">
|
||||
<property name="text">
|
||||
<string>Edit track information...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+E</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_renumber_tracks">
|
||||
<property name="text">
|
||||
<string>Renumber tracks in this order...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_selection_set_value">
|
||||
<property name="text">
|
||||
<string>Set value for all selected tracks...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_edit_value">
|
||||
<property name="text">
|
||||
<string>Edit tag...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F2</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_settings">
|
||||
<property name="text">
|
||||
<string>&Settings...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+P</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_about_strawberry">
|
||||
<property name="icon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&About Strawberry</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F1</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::AboutRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_shuffle">
|
||||
<property name="text">
|
||||
<string>S&huffle playlist</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+H</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_add_file">
|
||||
<property name="text">
|
||||
<string>&Add file...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_open_file">
|
||||
<property name="text">
|
||||
<string>&Open file...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_open_cd">
|
||||
<property name="text">
|
||||
<string>Open &audio CD...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_cover_manager">
|
||||
<property name="text">
|
||||
<string>&Cover Manager</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_console">
|
||||
<property name="text">
|
||||
<string>C&onsole</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_shuffle_mode">
|
||||
<property name="text">
|
||||
<string>&Shuffle mode</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_repeat_mode">
|
||||
<property name="text">
|
||||
<string>&Repeat mode</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_remove_from_playlist">
|
||||
<property name="text">
|
||||
<string>Remove from playlist</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_equalizer">
|
||||
<property name="text">
|
||||
<string>&Equalizer</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_add_folder">
|
||||
<property name="text">
|
||||
<string>Add &folder...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_jump">
|
||||
<property name="text">
|
||||
<string>&Jump to the currently playing track</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+J</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_new_playlist">
|
||||
<property name="text">
|
||||
<string>&New playlist</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+N</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_save_playlist">
|
||||
<property name="text">
|
||||
<string>Save &playlist...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_load_playlist">
|
||||
<property name="text">
|
||||
<string>&Load playlist...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_next_playlist">
|
||||
<property name="text">
|
||||
<string>Go to next playlist tab</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_previous_playlist">
|
||||
<property name="text">
|
||||
<string>Go to previous playlist tab</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_update_collection">
|
||||
<property name="text">
|
||||
<string>&Update changed collection folders</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_queue_manager">
|
||||
<property name="text">
|
||||
<string>&Queue Manager</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_about_qt">
|
||||
<property name="text">
|
||||
<string>About &Qt</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::AboutQtRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_mute">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Mute</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+M</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_full_collection_scan">
|
||||
<property name="text">
|
||||
<string>&Do a full collection rescan</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_auto_complete_tags">
|
||||
<property name="icon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/pictures/musicbrainz.png</normaloff>:/pictures/musicbrainz.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Complete tags automatically...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+T</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_toggle_scrobbling">
|
||||
<property name="text">
|
||||
<string>Toggle scrobbling</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_remove_duplicates">
|
||||
<property name="text">
|
||||
<string>Remove &duplicates from playlist</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_remove_unavailable">
|
||||
<property name="text">
|
||||
<string>Remove &unavailable tracks from playlist</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Amarok::VolumeSlider</class>
|
||||
<extends>QSlider</extends>
|
||||
<header>widgets/sliderwidget.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>AnalyzerContainer</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>analyzer/analyzercontainer.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PlaylistContainer</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>playlist/playlistcontainer.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>TrackSlider</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/trackslider.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PlaylistSequence</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>playlist/playlistsequence.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>MultiLoadingIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/multiloadingindicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PlayingWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/playingwidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>FancyTabWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/fancytabwidget.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
545
src/core/mergedproxymodel.cpp
Normal file
545
src/core/mergedproxymodel.cpp
Normal file
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "mergedproxymodel.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
// boost::multi_index still relies on these being in the global namespace.
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
|
||||
#include <boost/multi_index_container.hpp>
|
||||
#include <boost/multi_index/member.hpp>
|
||||
#include <boost/multi_index/hashed_index.hpp>
|
||||
#include <boost/multi_index/ordered_index.hpp>
|
||||
|
||||
using boost::multi_index::hashed_unique;
|
||||
using boost::multi_index::identity;
|
||||
using boost::multi_index::indexed_by;
|
||||
using boost::multi_index::member;
|
||||
using boost::multi_index::multi_index_container;
|
||||
using boost::multi_index::ordered_unique;
|
||||
using boost::multi_index::tag;
|
||||
|
||||
std::size_t hash_value(const QModelIndex& index) { return qHash(index); }
|
||||
|
||||
namespace {
|
||||
|
||||
struct Mapping {
|
||||
explicit Mapping(const QModelIndex& _source_index) : source_index(_source_index) {}
|
||||
|
||||
QModelIndex source_index;
|
||||
};
|
||||
|
||||
struct tag_by_source {};
|
||||
struct tag_by_pointer {};
|
||||
|
||||
} // namespace
|
||||
|
||||
class MergedProxyModelPrivate {
|
||||
private:
|
||||
typedef multi_index_container<
|
||||
Mapping*,
|
||||
indexed_by<
|
||||
hashed_unique<tag<tag_by_source>,
|
||||
member<Mapping, QModelIndex, &Mapping::source_index> >,
|
||||
ordered_unique<tag<tag_by_pointer>, identity<Mapping*> > > >
|
||||
MappingContainer;
|
||||
|
||||
public:
|
||||
MappingContainer mappings_;
|
||||
};
|
||||
|
||||
MergedProxyModel::MergedProxyModel(QObject* parent)
|
||||
: QAbstractProxyModel(parent),
|
||||
resetting_model_(nullptr),
|
||||
p_(new MergedProxyModelPrivate) {}
|
||||
|
||||
MergedProxyModel::~MergedProxyModel() { DeleteAllMappings(); }
|
||||
|
||||
void MergedProxyModel::DeleteAllMappings() {
|
||||
const auto& begin = p_->mappings_.get<tag_by_pointer>().begin();
|
||||
const auto& end = p_->mappings_.get<tag_by_pointer>().end();
|
||||
qDeleteAll(begin, end);
|
||||
}
|
||||
|
||||
void MergedProxyModel::AddSubModel(const QModelIndex& source_parent, QAbstractItemModel* submodel) {
|
||||
|
||||
connect(submodel, SIGNAL(modelReset()), this, SLOT(SubModelReset()));
|
||||
connect(submodel, SIGNAL(rowsAboutToBeInserted(QModelIndex, int, int)), this, SLOT(RowsAboutToBeInserted(QModelIndex, int, int)));
|
||||
connect(submodel, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(RowsAboutToBeRemoved(QModelIndex, int, int)));
|
||||
connect(submodel, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(RowsInserted(QModelIndex, int, int)));
|
||||
connect(submodel, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(RowsRemoved(QModelIndex, int, int)));
|
||||
connect(submodel, SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(DataChanged(QModelIndex, QModelIndex)));
|
||||
|
||||
QModelIndex proxy_parent = mapFromSource(source_parent);
|
||||
const int rows = submodel->rowCount();
|
||||
|
||||
if (rows) beginInsertRows(proxy_parent, 0, rows - 1);
|
||||
|
||||
merge_points_.insert(submodel, source_parent);
|
||||
|
||||
if (rows) endInsertRows();
|
||||
}
|
||||
|
||||
void MergedProxyModel::RemoveSubModel(const QModelIndex& source_parent) {
|
||||
// Find the submodel that the parent corresponded to
|
||||
QAbstractItemModel* submodel = merge_points_.key(source_parent);
|
||||
merge_points_.remove(submodel);
|
||||
|
||||
// The submodel might have been deleted already so we must be careful not
|
||||
// to dereference it.
|
||||
|
||||
// Remove all the children of the item that got deleted
|
||||
QModelIndex proxy_parent = mapFromSource(source_parent);
|
||||
|
||||
// We can't know how many children it had, since we can't dereference it
|
||||
resetting_model_ = submodel;
|
||||
beginRemoveRows(proxy_parent, 0, std::numeric_limits<int>::max() - 1);
|
||||
endRemoveRows();
|
||||
resetting_model_ = nullptr;
|
||||
|
||||
// Delete all the mappings that reference the submodel
|
||||
auto it = p_->mappings_.get<tag_by_pointer>().begin();
|
||||
auto end = p_->mappings_.get<tag_by_pointer>().end();
|
||||
while (it != end) {
|
||||
if ((*it)->source_index.model() == submodel) {
|
||||
delete *it;
|
||||
it = p_->mappings_.get<tag_by_pointer>().erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MergedProxyModel::setSourceModel(QAbstractItemModel* source_model) {
|
||||
|
||||
if (sourceModel()) {
|
||||
disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(SourceModelReset()));
|
||||
disconnect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(RowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
disconnect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(RowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(RowsInserted(QModelIndex,int,int)));
|
||||
disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(RowsRemoved(QModelIndex,int,int)));
|
||||
disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex, QModelIndex)), this, SLOT(DataChanged(QModelIndex, QModelIndex)));
|
||||
disconnect(sourceModel(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(LayoutAboutToBeChanged()));
|
||||
disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(LayoutChanged()));
|
||||
}
|
||||
|
||||
QAbstractProxyModel::setSourceModel(source_model);
|
||||
|
||||
connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(SourceModelReset()));
|
||||
connect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex, int, int)), this, SLOT(RowsAboutToBeInserted(QModelIndex, int, int)));
|
||||
connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), this, SLOT(RowsAboutToBeRemoved(QModelIndex, int, int)));
|
||||
connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(RowsInserted(QModelIndex,int,int)));
|
||||
connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(RowsRemoved(QModelIndex,int,int)));
|
||||
connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(DataChanged(QModelIndex,QModelIndex)));
|
||||
connect(sourceModel(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(LayoutAboutToBeChanged()));
|
||||
connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(LayoutChanged()));
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::SourceModelReset() {
|
||||
|
||||
// Delete all mappings
|
||||
DeleteAllMappings();
|
||||
|
||||
// Reset the proxy
|
||||
beginResetModel();
|
||||
|
||||
// Clear the containers
|
||||
p_->mappings_.clear();
|
||||
merge_points_.clear();
|
||||
|
||||
endResetModel();
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::SubModelReset() {
|
||||
|
||||
QAbstractItemModel* submodel = static_cast<QAbstractItemModel*>(sender());
|
||||
|
||||
// TODO: When we require Qt 4.6, use beginResetModel() and endResetModel()
|
||||
// in CollectionModel and catch those here - that will let us do away with this
|
||||
// std::numeric_limits<int>::max() hack.
|
||||
|
||||
// Remove all the children of the item that got deleted
|
||||
QModelIndex source_parent = merge_points_.value(submodel);
|
||||
QModelIndex proxy_parent = mapFromSource(source_parent);
|
||||
|
||||
// We can't know how many children it had, since it's already disappeared...
|
||||
resetting_model_ = submodel;
|
||||
beginRemoveRows(proxy_parent, 0, std::numeric_limits<int>::max() - 1);
|
||||
endRemoveRows();
|
||||
resetting_model_ = nullptr;
|
||||
|
||||
// Delete all the mappings that reference the submodel
|
||||
auto it = p_->mappings_.get<tag_by_pointer>().begin();
|
||||
auto end = p_->mappings_.get<tag_by_pointer>().end();
|
||||
while (it != end) {
|
||||
if ((*it)->source_index.model() == submodel) {
|
||||
delete *it;
|
||||
it = p_->mappings_.get<tag_by_pointer>().erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// "Insert" items from the newly reset submodel
|
||||
int count = submodel->rowCount();
|
||||
if (count) {
|
||||
beginInsertRows(proxy_parent, 0, count - 1);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
emit SubModelReset(proxy_parent, submodel);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::GetActualSourceParent(const QModelIndex& source_parent, QAbstractItemModel* model) const {
|
||||
|
||||
if (!source_parent.isValid() && model != sourceModel())
|
||||
return merge_points_.value(model);
|
||||
return source_parent;
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::RowsAboutToBeInserted(const QModelIndex& source_parent, int start, int end) {
|
||||
beginInsertRows(mapFromSource(GetActualSourceParent(source_parent, static_cast<QAbstractItemModel*>(sender()))), start, end);
|
||||
}
|
||||
|
||||
void MergedProxyModel::RowsInserted(const QModelIndex&, int, int) {
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void MergedProxyModel::RowsAboutToBeRemoved(const QModelIndex& source_parent, int start, int end) {
|
||||
beginRemoveRows(mapFromSource(GetActualSourceParent(source_parent, static_cast<QAbstractItemModel*>(sender()))), start, end);
|
||||
}
|
||||
|
||||
void MergedProxyModel::RowsRemoved(const QModelIndex&, int, int) {
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::mapToSource(const QModelIndex& proxy_index) const {
|
||||
|
||||
if (!proxy_index.isValid()) return QModelIndex();
|
||||
|
||||
Mapping* mapping = static_cast<Mapping*>(proxy_index.internalPointer());
|
||||
if (p_->mappings_.get<tag_by_pointer>().find(mapping) ==
|
||||
p_->mappings_.get<tag_by_pointer>().end())
|
||||
return QModelIndex();
|
||||
if (mapping->source_index.model() == resetting_model_) return QModelIndex();
|
||||
|
||||
return mapping->source_index;
|
||||
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::mapFromSource(const QModelIndex& source_index) const {
|
||||
|
||||
if (!source_index.isValid()) return QModelIndex();
|
||||
if (source_index.model() == resetting_model_) return QModelIndex();
|
||||
|
||||
// Add a mapping if we don't have one already
|
||||
const auto& it = p_->mappings_.get<tag_by_source>().find(source_index);
|
||||
Mapping* mapping;
|
||||
if (it != p_->mappings_.get<tag_by_source>().end()) {
|
||||
mapping = *it;
|
||||
} else {
|
||||
mapping = new Mapping(source_index);
|
||||
const_cast<MergedProxyModel*>(this)->p_->mappings_.insert(mapping);
|
||||
}
|
||||
|
||||
return createIndex(source_index.row(), source_index.column(), mapping);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::index(int row, int column, const QModelIndex &parent) const {
|
||||
|
||||
QModelIndex source_index;
|
||||
|
||||
if (!parent.isValid()) {
|
||||
source_index = sourceModel()->index(row, column, QModelIndex());
|
||||
}
|
||||
else {
|
||||
QModelIndex source_parent = mapToSource(parent);
|
||||
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
|
||||
|
||||
if (child_model)
|
||||
source_index = child_model->index(row, column, QModelIndex());
|
||||
else
|
||||
source_index = source_parent.model()->index(row, column, source_parent);
|
||||
}
|
||||
|
||||
return mapFromSource(source_index);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::parent(const QModelIndex& child) const {
|
||||
|
||||
QModelIndex source_child = mapToSource(child);
|
||||
if (source_child.model() == sourceModel())
|
||||
return mapFromSource(source_child.parent());
|
||||
|
||||
if (!IsKnownModel(source_child.model())) return QModelIndex();
|
||||
|
||||
if (!source_child.parent().isValid())
|
||||
return mapFromSource(merge_points_.value(GetModel(source_child)));
|
||||
return mapFromSource(source_child.parent());
|
||||
|
||||
}
|
||||
|
||||
int MergedProxyModel::rowCount(const QModelIndex& parent) const {
|
||||
|
||||
if (!parent.isValid()) return sourceModel()->rowCount(QModelIndex());
|
||||
|
||||
QModelIndex source_parent = mapToSource(parent);
|
||||
if (!IsKnownModel(source_parent.model())) return 0;
|
||||
|
||||
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
|
||||
if (child_model) {
|
||||
// Query the source model but disregard what it says, so it gets a chance
|
||||
// to lazy load
|
||||
source_parent.model()->rowCount(source_parent);
|
||||
|
||||
return child_model->rowCount(QModelIndex());
|
||||
}
|
||||
|
||||
return source_parent.model()->rowCount(source_parent);
|
||||
|
||||
}
|
||||
|
||||
int MergedProxyModel::columnCount(const QModelIndex& parent) const {
|
||||
|
||||
if (!parent.isValid()) return sourceModel()->columnCount(QModelIndex());
|
||||
|
||||
QModelIndex source_parent = mapToSource(parent);
|
||||
if (!IsKnownModel(source_parent.model())) return 0;
|
||||
|
||||
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
|
||||
if (child_model) return child_model->columnCount(QModelIndex());
|
||||
return source_parent.model()->columnCount(source_parent);
|
||||
|
||||
}
|
||||
|
||||
bool MergedProxyModel::hasChildren(const QModelIndex& parent) const {
|
||||
|
||||
if (!parent.isValid()) return sourceModel()->hasChildren(QModelIndex());
|
||||
|
||||
QModelIndex source_parent = mapToSource(parent);
|
||||
if (!IsKnownModel(source_parent.model())) return false;
|
||||
|
||||
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
|
||||
|
||||
if (child_model) return child_model->hasChildren(QModelIndex()) || source_parent.model()->hasChildren(source_parent);
|
||||
return source_parent.model()->hasChildren(source_parent);
|
||||
|
||||
}
|
||||
|
||||
QVariant MergedProxyModel::data(const QModelIndex& proxyIndex, int role) const {
|
||||
|
||||
QModelIndex source_index = mapToSource(proxyIndex);
|
||||
if (!IsKnownModel(source_index.model())) return QVariant();
|
||||
|
||||
return source_index.model()->data(source_index, role);
|
||||
|
||||
}
|
||||
|
||||
QMap<int, QVariant> MergedProxyModel::itemData(const QModelIndex& proxy_index) const {
|
||||
|
||||
QModelIndex source_index = mapToSource(proxy_index);
|
||||
|
||||
if (!source_index.isValid()) return sourceModel()->itemData(QModelIndex());
|
||||
return source_index.model()->itemData(source_index);
|
||||
|
||||
}
|
||||
|
||||
Qt::ItemFlags MergedProxyModel::flags(const QModelIndex& index) const {
|
||||
|
||||
QModelIndex source_index = mapToSource(index);
|
||||
|
||||
if (!source_index.isValid()) return sourceModel()->flags(QModelIndex());
|
||||
return source_index.model()->flags(source_index);
|
||||
|
||||
}
|
||||
|
||||
bool MergedProxyModel::setData(const QModelIndex& index, const QVariant& value, int role) {
|
||||
|
||||
QModelIndex source_index = mapToSource(index);
|
||||
|
||||
if (!source_index.isValid())
|
||||
return sourceModel()->setData(index, value, role);
|
||||
return GetModel(index)->setData(index, value, role);
|
||||
|
||||
}
|
||||
|
||||
QStringList MergedProxyModel::mimeTypes() const {
|
||||
|
||||
QStringList ret;
|
||||
ret << sourceModel()->mimeTypes();
|
||||
|
||||
for (const QAbstractItemModel* model : merge_points_.keys()) {
|
||||
ret << model->mimeTypes();
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QMimeData* MergedProxyModel::mimeData(const QModelIndexList& indexes) const {
|
||||
|
||||
if (indexes.isEmpty()) return 0;
|
||||
|
||||
// Only ask the first index's model
|
||||
const QAbstractItemModel* model = mapToSource(indexes[0]).model();
|
||||
if (!model) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only ask about the indexes that are actually in that model
|
||||
QModelIndexList indexes_in_model;
|
||||
|
||||
for (const QModelIndex& proxy_index : indexes) {
|
||||
QModelIndex source_index = mapToSource(proxy_index);
|
||||
if (source_index.model() != model) continue;
|
||||
indexes_in_model << source_index;
|
||||
}
|
||||
|
||||
return model->mimeData(indexes_in_model);
|
||||
|
||||
}
|
||||
|
||||
bool MergedProxyModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) {
|
||||
|
||||
if (!parent.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return sourceModel()->dropMimeData(data, action, row, column, parent);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex MergedProxyModel::FindSourceParent(const QModelIndex& proxy_index) const {
|
||||
|
||||
if (!proxy_index.isValid()) return QModelIndex();
|
||||
|
||||
QModelIndex source_index = mapToSource(proxy_index);
|
||||
if (source_index.model() == sourceModel()) return source_index;
|
||||
return merge_points_.value(GetModel(source_index));
|
||||
|
||||
}
|
||||
|
||||
bool MergedProxyModel::canFetchMore(const QModelIndex& parent) const {
|
||||
|
||||
QModelIndex source_index = mapToSource(parent);
|
||||
|
||||
if (!source_index.isValid())
|
||||
return sourceModel()->canFetchMore(QModelIndex());
|
||||
return source_index.model()->canFetchMore(source_index);
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::fetchMore(const QModelIndex& parent) {
|
||||
|
||||
QModelIndex source_index = mapToSource(parent);
|
||||
|
||||
if (!source_index.isValid())
|
||||
sourceModel()->fetchMore(QModelIndex());
|
||||
else
|
||||
GetModel(source_index)->fetchMore(source_index);
|
||||
|
||||
}
|
||||
|
||||
QAbstractItemModel* MergedProxyModel::GetModel(const QModelIndex& source_index) const {
|
||||
|
||||
// This is essentially const_cast<QAbstractItemModel*>(source_index.model()),
|
||||
// but without the const_cast
|
||||
const QAbstractItemModel* const_model = source_index.model();
|
||||
if (const_model == sourceModel()) return sourceModel();
|
||||
for (QAbstractItemModel* submodel : merge_points_.keys()) {
|
||||
if (submodel == const_model) return submodel;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::DataChanged(const QModelIndex& top_left, const QModelIndex& bottom_right) {
|
||||
emit dataChanged(mapFromSource(top_left), mapFromSource(bottom_right));
|
||||
}
|
||||
|
||||
void MergedProxyModel::LayoutAboutToBeChanged() {
|
||||
|
||||
old_merge_points_.clear();
|
||||
for (QAbstractItemModel* key : merge_points_.keys()) {
|
||||
old_merge_points_[key] = merge_points_.value(key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void MergedProxyModel::LayoutChanged() {
|
||||
|
||||
for (QAbstractItemModel* key : merge_points_.keys()) {
|
||||
if (!old_merge_points_.contains(key)) continue;
|
||||
|
||||
const int old_row = old_merge_points_[key].row();
|
||||
const int new_row = merge_points_[key].row();
|
||||
|
||||
if (old_row != new_row) {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool MergedProxyModel::IsKnownModel(const QAbstractItemModel* model) const {
|
||||
|
||||
if (model == this || model == sourceModel() ||
|
||||
merge_points_.contains(const_cast<QAbstractItemModel*>(model)))
|
||||
return true;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
QModelIndexList MergedProxyModel::mapFromSource(const QModelIndexList& source_indexes) const {
|
||||
|
||||
QModelIndexList ret;
|
||||
for (const QModelIndex& index : source_indexes) {
|
||||
ret << mapFromSource(index);
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QModelIndexList MergedProxyModel::mapToSource(const QModelIndexList& proxy_indexes) const {
|
||||
|
||||
QModelIndexList ret;
|
||||
for (const QModelIndex& index : proxy_indexes) {
|
||||
ret << mapToSource(index);
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
110
src/core/mergedproxymodel.h
Normal file
110
src/core/mergedproxymodel.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MERGEDPROXYMODEL_H
|
||||
#define MERGEDPROXYMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QAbstractProxyModel>
|
||||
|
||||
std::size_t hash_value(const QModelIndex& index);
|
||||
|
||||
class MergedProxyModelPrivate;
|
||||
|
||||
class MergedProxyModel : public QAbstractProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MergedProxyModel(QObject* parent = nullptr);
|
||||
~MergedProxyModel();
|
||||
|
||||
// Make another model appear as a child of the given item in the source model.
|
||||
void AddSubModel(const QModelIndex& source_parent, QAbstractItemModel* submodel);
|
||||
void RemoveSubModel(const QModelIndex& source_parent);
|
||||
|
||||
// Find the item in the source model that is the parent of the model
|
||||
// containing proxy_index. If proxy_index is in the source model, then
|
||||
// this just returns mapToSource(proxy_index).
|
||||
QModelIndex FindSourceParent(const QModelIndex& proxy_index) const;
|
||||
|
||||
// QAbstractItemModel
|
||||
QModelIndex index(int row, int column, const QModelIndex& parent) const;
|
||||
QModelIndex parent(const QModelIndex& child) const;
|
||||
int rowCount(const QModelIndex& parent) const;
|
||||
int columnCount(const QModelIndex& parent) const;
|
||||
QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const;
|
||||
bool hasChildren(const QModelIndex& parent) const;
|
||||
QMap<int, QVariant> itemData(const QModelIndex& proxyIndex) const;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role);
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData* mimeData(const QModelIndexList& indexes) const;
|
||||
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
|
||||
bool canFetchMore(const QModelIndex& parent) const;
|
||||
void fetchMore(const QModelIndex& parent);
|
||||
|
||||
// QAbstractProxyModel
|
||||
// Note that these implementations of map{To,From}Source will not always
|
||||
// give you an index in sourceModel(), you might get an index in one of the
|
||||
// child models instead.
|
||||
QModelIndex mapFromSource(const QModelIndex& sourceIndex) const;
|
||||
QModelIndex mapToSource(const QModelIndex& proxyIndex) const;
|
||||
void setSourceModel(QAbstractItemModel* sourceModel);
|
||||
|
||||
// Convenience functions that call map{To,From}Source multiple times.
|
||||
QModelIndexList mapFromSource(const QModelIndexList& source_indexes) const;
|
||||
QModelIndexList mapToSource(const QModelIndexList& proxy_indexes) const;
|
||||
|
||||
signals:
|
||||
void SubModelReset(const QModelIndex& root, QAbstractItemModel* model);
|
||||
|
||||
private slots:
|
||||
void SourceModelReset();
|
||||
void SubModelReset();
|
||||
|
||||
void RowsAboutToBeInserted(const QModelIndex& source_parent, int start, int end);
|
||||
void RowsInserted(const QModelIndex& source_parent, int start, int end);
|
||||
void RowsAboutToBeRemoved(const QModelIndex& source_parent, int start, int end);
|
||||
void RowsRemoved(const QModelIndex& source_parent, int start, int end);
|
||||
void DataChanged(const QModelIndex& top_left, const QModelIndex& bottom_right);
|
||||
|
||||
void LayoutAboutToBeChanged();
|
||||
void LayoutChanged();
|
||||
|
||||
private:
|
||||
QModelIndex GetActualSourceParent(const QModelIndex& source_parent,
|
||||
QAbstractItemModel* model) const;
|
||||
QAbstractItemModel* GetModel(const QModelIndex& source_index) const;
|
||||
void DeleteAllMappings();
|
||||
bool IsKnownModel(const QAbstractItemModel* model) const;
|
||||
|
||||
QMap<QAbstractItemModel*, QPersistentModelIndex> merge_points_;
|
||||
QAbstractItemModel* resetting_model_;
|
||||
|
||||
QMap<QAbstractItemModel*, QModelIndex> old_merge_points_;
|
||||
|
||||
std::unique_ptr<MergedProxyModelPrivate> p_;
|
||||
};
|
||||
|
||||
#endif // MERGEDPROXYMODEL_H
|
||||
|
||||
107
src/core/metatypes.cpp
Normal file
107
src/core/metatypes.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "metatypes.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <QMetaType>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkCookie>
|
||||
|
||||
#include "metatypes.h"
|
||||
|
||||
#include "engine/enginebase.h"
|
||||
#include "engine/enginetype.h"
|
||||
#ifdef HAVE_GSTREAMER
|
||||
# include "engine/gstengine.h"
|
||||
# include "engine/gstenginepipeline.h"
|
||||
#endif
|
||||
#include "collection/directory.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "equalizer/equalizer.h"
|
||||
#include "covermanager/albumcoverfetcher.h"
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
#include <QDBusMetaType>
|
||||
#include "core/mpris2.h"
|
||||
#include "dbus/metatypes.h"
|
||||
#endif
|
||||
|
||||
class QNetworkReply;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
class GstEnginePipeline;
|
||||
#endif
|
||||
|
||||
void RegisterMetaTypes() {
|
||||
//qRegisterMetaType<CollapsibleInfoPane::Data>("CollapsibleInfoPane::Data");
|
||||
qRegisterMetaType<const char*>("const char*");
|
||||
qRegisterMetaType<CoverSearchResult>("CoverSearchResult");
|
||||
qRegisterMetaType<CoverSearchResults>("CoverSearchResults");
|
||||
qRegisterMetaType<Directory>("Directory");
|
||||
qRegisterMetaType<DirectoryList>("DirectoryList");
|
||||
qRegisterMetaType<Engine::SimpleMetaBundle>("Engine::SimpleMetaBundle");
|
||||
qRegisterMetaType<Engine::State>("Engine::State");
|
||||
qRegisterMetaType<Engine::TrackChangeFlags>("Engine::TrackChangeFlags");
|
||||
qRegisterMetaType<Equalizer::Params>("Equalizer::Params");
|
||||
qRegisterMetaType<EngineBase::PluginDetails>("EngineBase::PluginDetails");
|
||||
qRegisterMetaType<EngineBase::OutputDetails>("EngineBase::OutputDetails");
|
||||
#ifdef HAVE_GSTREAMER
|
||||
qRegisterMetaType<GstBuffer*>("GstBuffer*");
|
||||
qRegisterMetaType<GstElement*>("GstElement*");
|
||||
qRegisterMetaType<GstEnginePipeline*>("GstEnginePipeline*");
|
||||
#endif
|
||||
qRegisterMetaType<PlaylistItemList>("PlaylistItemList");
|
||||
qRegisterMetaType<PlaylistItemPtr>("PlaylistItemPtr");
|
||||
qRegisterMetaType<QList<CoverSearchResult> >("QList<CoverSearchResult>");
|
||||
qRegisterMetaType<QList<int>>("QList<int>");
|
||||
qRegisterMetaType<QList<PlaylistItemPtr> >("QList<PlaylistItemPtr>");
|
||||
qRegisterMetaType<PlaylistSequence::RepeatMode>("PlaylistSequence::RepeatMode");
|
||||
qRegisterMetaType<PlaylistSequence::ShuffleMode>("PlaylistSequence::ShuffleMode");
|
||||
qRegisterMetaType<QAbstractSocket::SocketState>("QAbstractSocket::SocketState");
|
||||
qRegisterMetaType<QList<QNetworkCookie> >("QList<QNetworkCookie>");
|
||||
qRegisterMetaType<QList<Song> >("QList<Song>");
|
||||
qRegisterMetaType<QNetworkCookie>("QNetworkCookie");
|
||||
qRegisterMetaType<QNetworkReply*>("QNetworkReply*");
|
||||
qRegisterMetaType<QNetworkReply**>("QNetworkReply**");
|
||||
qRegisterMetaType<SongList>("SongList");
|
||||
qRegisterMetaType<Song>("Song");
|
||||
qRegisterMetaTypeStreamOperators<Equalizer::Params>("Equalizer::Params");
|
||||
qRegisterMetaTypeStreamOperators<QMap<int, int> >("ColumnAlignmentMap");
|
||||
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
|
||||
qRegisterMetaType<Subdirectory>("Subdirectory");
|
||||
qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
|
||||
qRegisterMetaType<QAbstractSocket::SocketState>();
|
||||
|
||||
qRegisterMetaType<Engine::EngineType>("EngineType");
|
||||
|
||||
#ifdef HAVE_DBUS
|
||||
qDBusRegisterMetaType<QImage>();
|
||||
qDBusRegisterMetaType<TrackMetadata>();
|
||||
qDBusRegisterMetaType<TrackIds>();
|
||||
qDBusRegisterMetaType<QList<QByteArray>>();
|
||||
qDBusRegisterMetaType<MprisPlaylist>();
|
||||
qDBusRegisterMetaType<MaybePlaylist>();
|
||||
qDBusRegisterMetaType<MprisPlaylistList>();
|
||||
qDBusRegisterMetaType<InterfacesAndProperties>();
|
||||
qDBusRegisterMetaType<ManagedObjectList>();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
6
src/core/metatypes.h
Normal file
6
src/core/metatypes.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#ifndef METATYPES_H
|
||||
#define METATYPES_H
|
||||
|
||||
void RegisterMetaTypes();
|
||||
|
||||
#endif
|
||||
76
src/core/mimedata.h
Normal file
76
src/core/mimedata.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MIMEDATA_H
|
||||
#define MIMEDATA_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMimeData>
|
||||
|
||||
class MimeData : public QMimeData {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MimeData(bool clear = false, bool play_now = false, bool enqueue = false, bool open_in_new_playlist = false)
|
||||
: override_user_settings_(false),
|
||||
clear_first_(clear),
|
||||
play_now_(play_now),
|
||||
enqueue_now_(enqueue),
|
||||
open_in_new_playlist_(open_in_new_playlist),
|
||||
name_for_new_playlist_(QString()),
|
||||
from_doubleclick_(false) {}
|
||||
|
||||
// If this is set then MainWindow will not touch any of the other flags.
|
||||
bool override_user_settings_;
|
||||
|
||||
// If this is set then the playlist will be cleared before these songs
|
||||
// are inserted.
|
||||
bool clear_first_;
|
||||
|
||||
// If this is set then the first item that is inserted will start playing
|
||||
// immediately. Note: this is always overridden with the user's preference
|
||||
// if the MimeData goes via MainWindow, unless you set override_user_settings_.
|
||||
bool play_now_;
|
||||
|
||||
// If this is set then the items are added to the queue after being inserted.
|
||||
bool enqueue_now_;
|
||||
|
||||
// If this is set then the items are inserted into a newly created playlist.
|
||||
bool open_in_new_playlist_;
|
||||
|
||||
// This serves as a name for the new playlist in 'open_in_new_playlist_' mode.
|
||||
QString name_for_new_playlist_;
|
||||
|
||||
// This can be set if this MimeData goes via MainWindow (ie. it is created
|
||||
// manually in a double-click). The MainWindow will set the above flags to
|
||||
// the defaults set by the user.
|
||||
bool from_doubleclick_;
|
||||
|
||||
// Returns a pretty name for a playlist containing songs described by this MimeData
|
||||
// object. By pretty name we mean the value of 'name_for_new_playlist_' or generic
|
||||
// "Playlist" string if the 'name_for_new_playlist_' attribute is empty.
|
||||
QString get_name_for_new_playlist() {
|
||||
return name_for_new_playlist_.isEmpty() ? tr("Playlist") : name_for_new_playlist_;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // MIMEDATA_H
|
||||
|
||||
33
src/core/mpris.cpp
Normal file
33
src/core/mpris.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "mpris.h"
|
||||
#include "mpris2.h"
|
||||
|
||||
namespace mpris {
|
||||
|
||||
Mpris::Mpris(Application* app, QObject* parent) : QObject(parent), mpris2_(new mpris::Mpris2(app, this)) {
|
||||
connect(mpris2_, SIGNAL(RaiseMainWindow()), SIGNAL(RaiseMainWindow()));
|
||||
}
|
||||
|
||||
} // namespace mpris
|
||||
52
src/core/mpris.h
Normal file
52
src/core/mpris.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MPRIS_H
|
||||
#define MPRIS_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class Application;
|
||||
|
||||
namespace mpris {
|
||||
|
||||
class Mpris1;
|
||||
class Mpris2;
|
||||
|
||||
class Mpris : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Mpris(Application *app, QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void RaiseMainWindow();
|
||||
|
||||
private:
|
||||
Mpris1 *mpris1_;
|
||||
Mpris2 *mpris2_;
|
||||
};
|
||||
|
||||
} // namespace mpris
|
||||
|
||||
#endif // MPRIS_H
|
||||
|
||||
543
src/core/mpris2.cpp
Normal file
543
src/core/mpris2.cpp
Normal file
@@ -0,0 +1,543 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 <algorithm>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "mpris2.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/mainwindow.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mpris_common.h"
|
||||
#include "core/mpris2_player.h"
|
||||
#include "core/mpris2_playlists.h"
|
||||
#include "core/mpris2_root.h"
|
||||
#include "core/mpris2_tracklist.h"
|
||||
#include "core/player.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "engine/enginebase.h"
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "playlist/playlistsequence.h"
|
||||
#include "covermanager/currentartloader.h"
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &arg, const MprisPlaylist &playlist) {
|
||||
arg.beginStructure();
|
||||
arg << playlist.id << playlist.name << playlist.icon;
|
||||
arg.endStructure();
|
||||
return arg;
|
||||
}
|
||||
|
||||
const QDBusArgument &operator>> (const QDBusArgument &arg, MprisPlaylist &playlist) {
|
||||
arg.beginStructure();
|
||||
arg >> playlist.id >> playlist.name >> playlist.icon;
|
||||
arg.endStructure();
|
||||
return arg;
|
||||
}
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &arg, const MaybePlaylist &playlist) {
|
||||
arg.beginStructure();
|
||||
arg << playlist.valid;
|
||||
arg << playlist.playlist;
|
||||
arg.endStructure();
|
||||
return arg;
|
||||
}
|
||||
|
||||
const QDBusArgument &operator>> (const QDBusArgument &arg, MaybePlaylist &playlist) {
|
||||
arg.beginStructure();
|
||||
arg >> playlist.valid >> playlist.playlist;
|
||||
arg.endStructure();
|
||||
return arg;
|
||||
}
|
||||
|
||||
namespace mpris {
|
||||
|
||||
const char* Mpris2::kMprisObjectPath = "/org/mpris/MediaPlayer2";
|
||||
const char* Mpris2::kServiceName = "org.mpris.MediaPlayer2.strawberry";
|
||||
const char* Mpris2::kFreedesktopPath = "org.freedesktop.DBus.Properties";
|
||||
|
||||
Mpris2::Mpris2(Application* app, QObject* parent) : QObject(parent), app_(app) {
|
||||
|
||||
new Mpris2Root(this);
|
||||
new Mpris2TrackList(this);
|
||||
new Mpris2Player(this);
|
||||
new Mpris2Playlists(this);
|
||||
|
||||
if (!QDBusConnection::sessionBus().registerService(kServiceName)) {
|
||||
qLog(Warning) << "Failed to register" << QString(kServiceName) << "on the session bus";
|
||||
return;
|
||||
}
|
||||
|
||||
QDBusConnection::sessionBus().registerObject(kMprisObjectPath, this);
|
||||
|
||||
connect(app_->current_art_loader(), SIGNAL(ArtLoaded(Song,QString,QImage)), SLOT(ArtLoaded(Song,QString)));
|
||||
|
||||
connect(app_->player()->engine(), SIGNAL(StateChanged(Engine::State)), SLOT(EngineStateChanged(Engine::State)));
|
||||
connect(app_->player(), SIGNAL(VolumeChanged(int)), SLOT(VolumeChanged()));
|
||||
connect(app_->player(), SIGNAL(Seeked(qlonglong)), SIGNAL(Seeked(qlonglong)));
|
||||
|
||||
connect(app_->playlist_manager(), SIGNAL(PlaylistManagerInitialized()), SLOT(PlaylistManagerInitialized()));
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
|
||||
connect(app_->playlist_manager(), SIGNAL(PlaylistChanged(Playlist*)), SLOT(PlaylistChanged(Playlist*)));
|
||||
connect(app_->playlist_manager(), SIGNAL(CurrentChanged(Playlist*)), SLOT(PlaylistCollectionChanged(Playlist*)));
|
||||
|
||||
}
|
||||
|
||||
// when PlaylistManager gets it ready, we connect PlaylistSequence with this
|
||||
void Mpris2::PlaylistManagerInitialized() {
|
||||
connect(app_->playlist_manager()->sequence(), SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)), SLOT(ShuffleModeChanged()));
|
||||
connect(app_->playlist_manager()->sequence(), SIGNAL(RepeatModeChanged(PlaylistSequence::RepeatMode)), SLOT(RepeatModeChanged()));
|
||||
}
|
||||
|
||||
void Mpris2::EngineStateChanged(Engine::State newState) {
|
||||
|
||||
if (newState != Engine::Playing && newState != Engine::Paused) {
|
||||
last_metadata_ = QVariantMap();
|
||||
EmitNotification("Metadata");
|
||||
}
|
||||
|
||||
EmitNotification("PlaybackStatus", PlaybackStatus(newState));
|
||||
if (newState == Engine::Playing) EmitNotification("CanSeek", CanSeek(newState));
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::VolumeChanged() { EmitNotification("Volume"); }
|
||||
|
||||
void Mpris2::ShuffleModeChanged() { EmitNotification("Shuffle"); }
|
||||
|
||||
void Mpris2::RepeatModeChanged() {
|
||||
|
||||
EmitNotification("LoopStatus");
|
||||
EmitNotification("CanGoNext", CanGoNext());
|
||||
EmitNotification("CanGoPrevious", CanGoPrevious());
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::EmitNotification(const QString &name, const QVariant &val) {
|
||||
EmitNotification(name, val, "org.mpris.MediaPlayer2.Player");
|
||||
}
|
||||
|
||||
void Mpris2::EmitNotification(const QString &name, const QVariant &val, const QString &mprisEntity) {
|
||||
|
||||
QDBusMessage msg = QDBusMessage::createSignal(kMprisObjectPath, kFreedesktopPath, "PropertiesChanged");
|
||||
QVariantMap map;
|
||||
map.insert(name, val);
|
||||
QVariantList args = QVariantList() << mprisEntity << map << QStringList();
|
||||
msg.setArguments(args);
|
||||
QDBusConnection::sessionBus().send(msg);
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::EmitNotification(const QString &name) {
|
||||
|
||||
QVariant value;
|
||||
if (name == "PlaybackStatus") value = PlaybackStatus();
|
||||
else if (name == "LoopStatus") value = LoopStatus();
|
||||
else if (name == "Shuffle") value = Shuffle();
|
||||
else if (name == "Metadata") value = Metadata();
|
||||
else if (name == "Volume") value = Volume();
|
||||
else if (name == "Position") value = Position();
|
||||
else if (name == "CanGoNext") value = CanGoNext();
|
||||
else if (name == "CanGoPrevious") value = CanGoPrevious();
|
||||
else if (name == "CanSeek") value = CanSeek();
|
||||
|
||||
if (value.isValid()) EmitNotification(name, value);
|
||||
|
||||
}
|
||||
|
||||
//------------------Root Interface--------------------------//
|
||||
|
||||
bool Mpris2::CanQuit() const { return true; }
|
||||
|
||||
bool Mpris2::CanRaise() const { return true; }
|
||||
|
||||
bool Mpris2::HasTrackList() const { return true; }
|
||||
|
||||
QString Mpris2::Identity() const { return QCoreApplication::applicationName(); }
|
||||
|
||||
QString Mpris2::DesktopEntryAbsolutePath() const {
|
||||
QStringList xdg_data_dirs = QString(getenv("XDG_DATA_DIRS")).split(":");
|
||||
xdg_data_dirs.append("/usr/local/share/");
|
||||
xdg_data_dirs.append("/usr/share/");
|
||||
|
||||
for (const QString &directory : xdg_data_dirs) {
|
||||
QString path = QString("%1/applications/%2.desktop").arg(
|
||||
directory, QApplication::applicationName().toLower());
|
||||
if (QFile::exists(path)) return path;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString Mpris2::DesktopEntry() const {
|
||||
return QApplication::applicationName().toLower();
|
||||
}
|
||||
|
||||
QStringList Mpris2::SupportedUriSchemes() const {
|
||||
|
||||
static QStringList res = QStringList() << "file"
|
||||
<< "http"
|
||||
<< "cdda"
|
||||
<< "smb"
|
||||
<< "sftp";
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
QStringList Mpris2::SupportedMimeTypes() const {
|
||||
|
||||
static QStringList res = QStringList() << "application/ogg"
|
||||
<< "application/x-ogg"
|
||||
<< "application/x-ogm-audio"
|
||||
<< "audio/aac"
|
||||
<< "audio/mp4"
|
||||
<< "audio/mpeg"
|
||||
<< "audio/mpegurl"
|
||||
<< "audio/ogg"
|
||||
<< "audio/vnd.rn-realaudio"
|
||||
<< "audio/vorbis"
|
||||
<< "audio/x-flac"
|
||||
<< "audio/x-mp3"
|
||||
<< "audio/x-mpeg"
|
||||
<< "audio/x-mpegurl"
|
||||
<< "audio/x-ms-wma"
|
||||
<< "audio/x-musepack"
|
||||
<< "audio/x-oggflac"
|
||||
<< "audio/x-pn-realaudio"
|
||||
<< "audio/x-scpls"
|
||||
<< "audio/x-speex"
|
||||
<< "audio/x-vorbis"
|
||||
<< "audio/x-vorbis+ogg"
|
||||
<< "audio/x-wav"
|
||||
<< "video/x-ms-asf"
|
||||
<< "x-content/audio-player";
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::Raise() { emit RaiseMainWindow(); }
|
||||
|
||||
void Mpris2::Quit() { qApp->quit(); }
|
||||
|
||||
QString Mpris2::PlaybackStatus() const {
|
||||
return PlaybackStatus(app_->player()->GetState());
|
||||
}
|
||||
|
||||
QString Mpris2::PlaybackStatus(Engine::State state) const {
|
||||
|
||||
switch (state) {
|
||||
case Engine::Playing: return "Playing";
|
||||
case Engine::Paused: return "Paused";
|
||||
default: return "Stopped";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString Mpris2::LoopStatus() const {
|
||||
|
||||
if (!app_->playlist_manager()->sequence()) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
switch (app_->playlist_manager()->sequence()->repeat_mode()) {
|
||||
case PlaylistSequence::Repeat_Album:
|
||||
case PlaylistSequence::Repeat_Playlist: return "Playlist";
|
||||
case PlaylistSequence::Repeat_Track: return "Track";
|
||||
default: return "None";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::SetLoopStatus(const QString &value) {
|
||||
|
||||
PlaylistSequence::RepeatMode mode = PlaylistSequence::Repeat_Off;
|
||||
|
||||
if (value == "None") {
|
||||
mode = PlaylistSequence::Repeat_Off;
|
||||
} else if (value == "Track") {
|
||||
mode = PlaylistSequence::Repeat_Track;
|
||||
} else if (value == "Playlist") {
|
||||
mode = PlaylistSequence::Repeat_Playlist;
|
||||
}
|
||||
|
||||
app_->playlist_manager()->active()->sequence()->SetRepeatMode(mode);
|
||||
|
||||
}
|
||||
|
||||
double Mpris2::Rate() const { return 1.0; }
|
||||
|
||||
void Mpris2::SetRate(double rate) {
|
||||
|
||||
if (rate == 0) {
|
||||
app_->player()->Pause();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Mpris2::Shuffle() const {
|
||||
|
||||
return app_->playlist_manager()->sequence()->shuffle_mode() != PlaylistSequence::Shuffle_Off;
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::SetShuffle(bool enable) {
|
||||
app_->playlist_manager()->active()->sequence()->SetShuffleMode(enable ? PlaylistSequence::Shuffle_All : PlaylistSequence::Shuffle_Off);
|
||||
}
|
||||
|
||||
QVariantMap Mpris2::Metadata() const { return last_metadata_; }
|
||||
|
||||
QString Mpris2::current_track_id() const {
|
||||
return QString("/org/strawberry/strawberrymusicplayer/Track/%1").arg(QString::number(app_->playlist_manager()->active()->current_row()));
|
||||
}
|
||||
|
||||
// We send Metadata change notification as soon as the process of
|
||||
// changing song starts...
|
||||
void Mpris2::CurrentSongChanged(const Song &song) {
|
||||
|
||||
ArtLoaded(song, "");
|
||||
EmitNotification("CanGoNext", CanGoNext());
|
||||
EmitNotification("CanGoPrevious", CanGoPrevious());
|
||||
EmitNotification("CanSeek", CanSeek());
|
||||
|
||||
}
|
||||
|
||||
// ... and we add the cover information later, when it's available.
|
||||
void Mpris2::ArtLoaded(const Song &song, const QString &art_uri) {
|
||||
|
||||
last_metadata_ = QVariantMap();
|
||||
song.ToXesam(&last_metadata_);
|
||||
|
||||
using mpris::AddMetadata;
|
||||
AddMetadata("mpris:trackid", current_track_id(), &last_metadata_);
|
||||
|
||||
if (!art_uri.isEmpty()) {
|
||||
AddMetadata("mpris:artUrl", art_uri, &last_metadata_);
|
||||
}
|
||||
|
||||
AddMetadata("year", song.year(), &last_metadata_);
|
||||
AddMetadata("bitrate", song.bitrate(), &last_metadata_);
|
||||
|
||||
EmitNotification("Metadata", last_metadata_);
|
||||
|
||||
}
|
||||
|
||||
double Mpris2::Volume() const { return app_->player()->GetVolume() / 100.0; }
|
||||
|
||||
void Mpris2::SetVolume(double value) { app_->player()->SetVolume(value * 100); }
|
||||
|
||||
qlonglong Mpris2::Position() const {
|
||||
return app_->player()->engine()->position_nanosec() / kNsecPerUsec;
|
||||
}
|
||||
|
||||
double Mpris2::MaximumRate() const { return 1.0; }
|
||||
|
||||
double Mpris2::MinimumRate() const { return 1.0; }
|
||||
|
||||
bool Mpris2::CanGoNext() const {
|
||||
return app_->playlist_manager()->active() && app_->playlist_manager()->active()->next_row() != -1;
|
||||
}
|
||||
|
||||
bool Mpris2::CanGoPrevious() const {
|
||||
return app_->playlist_manager()->active() && (app_->playlist_manager()->active()->previous_row() != -1 || app_->player()->PreviousWouldRestartTrack());
|
||||
}
|
||||
|
||||
bool Mpris2::CanPlay() const {
|
||||
return app_->playlist_manager()->active() && app_->playlist_manager()->active()->rowCount() != 0 && !(app_->player()->GetState() == Engine::Playing);
|
||||
}
|
||||
|
||||
// This one's a bit different than MPRIS 1 - we want this to be true even when
|
||||
// the song is already paused or stopped.
|
||||
bool Mpris2::CanPause() const {
|
||||
return (app_->player()->GetCurrentItem() && app_->player()->GetState() == Engine::Playing && !(app_->player()->GetCurrentItem()->options() & PlaylistItem::PauseDisabled)) || PlaybackStatus() == "Paused" || PlaybackStatus() == "Stopped";
|
||||
}
|
||||
|
||||
bool Mpris2::CanSeek() const { return CanSeek(app_->player()->GetState()); }
|
||||
|
||||
bool Mpris2::CanSeek(Engine::State state) const {
|
||||
return app_->player()->GetCurrentItem() && state != Engine::Empty;
|
||||
}
|
||||
|
||||
bool Mpris2::CanControl() const { return true; }
|
||||
|
||||
void Mpris2::Next() {
|
||||
if (CanGoNext()) {
|
||||
app_->player()->Next();
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::Previous() {
|
||||
if (CanGoPrevious()) {
|
||||
app_->player()->Previous();
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::Pause() {
|
||||
if (CanPause() && app_->player()->GetState() != Engine::Paused) {
|
||||
app_->player()->Pause();
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::PlayPause() {
|
||||
if (CanPause()) {
|
||||
app_->player()->PlayPause();
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::Stop() { app_->player()->Stop(); }
|
||||
|
||||
void Mpris2::Play() {
|
||||
if (CanPlay()) {
|
||||
app_->player()->Play();
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::Seek(qlonglong offset) {
|
||||
if (CanSeek()) {
|
||||
app_->player()->SeekTo(app_->player()->engine()->position_nanosec() / kNsecPerSec + offset / kUsecPerSec);
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::SetPosition(const QDBusObjectPath &trackId, qlonglong offset) {
|
||||
if (CanSeek() && trackId.path() == current_track_id() && offset >= 0) {
|
||||
offset *= kNsecPerUsec;
|
||||
|
||||
if(offset < app_->player()->GetCurrentItem()->Metadata().length_nanosec()) {
|
||||
app_->player()->SeekTo(offset / kNsecPerSec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Mpris2::OpenUri(const QString &uri) {
|
||||
app_->playlist_manager()->active()->InsertUrls(QList<QUrl>() << QUrl(uri), -1, true);
|
||||
}
|
||||
|
||||
TrackIds Mpris2::Tracks() const {
|
||||
// TODO
|
||||
return TrackIds();
|
||||
}
|
||||
|
||||
bool Mpris2::CanEditTracks() const { return false; }
|
||||
|
||||
TrackMetadata Mpris2::GetTracksMetadata(const TrackIds &tracks) const {
|
||||
// TODO
|
||||
return TrackMetadata();
|
||||
}
|
||||
|
||||
void Mpris2::AddTrack(const QString &uri, const QDBusObjectPath &afterTrack, bool setAsCurrent) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void Mpris2::RemoveTrack(const QDBusObjectPath &trackId) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void Mpris2::GoTo(const QDBusObjectPath &trackId) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
quint32 Mpris2::PlaylistCount() const {
|
||||
return app_->playlist_manager()->GetAllPlaylists().size();
|
||||
}
|
||||
|
||||
QStringList Mpris2::Orderings() const { return QStringList() << "User"; }
|
||||
|
||||
namespace {
|
||||
|
||||
QDBusObjectPath MakePlaylistPath(int id) {
|
||||
return QDBusObjectPath(QString("/org/strawberry/strawberrymusicplayer/PlaylistId/%1").arg(id));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MaybePlaylist Mpris2::ActivePlaylist() const {
|
||||
|
||||
MaybePlaylist maybe_playlist;
|
||||
Playlist* current_playlist = app_->playlist_manager()->current();
|
||||
maybe_playlist.valid = current_playlist;
|
||||
if (!current_playlist) {
|
||||
return maybe_playlist;
|
||||
}
|
||||
|
||||
maybe_playlist.playlist.id = MakePlaylistPath(current_playlist->id());
|
||||
maybe_playlist.playlist.name = app_->playlist_manager()->GetPlaylistName(current_playlist->id());
|
||||
return maybe_playlist;
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::ActivatePlaylist(const QDBusObjectPath &playlist_id) {
|
||||
|
||||
QStringList split_path = playlist_id.path().split('/');
|
||||
qLog(Debug) << Q_FUNC_INFO << playlist_id.path() << split_path;
|
||||
if (split_path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
int p = split_path.last().toInt(&ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
if (!app_->playlist_manager()->IsPlaylistOpen(p)) {
|
||||
qLog(Error) << "Playlist isn't opened!";
|
||||
return;
|
||||
}
|
||||
app_->playlist_manager()->SetActivePlaylist(p);
|
||||
app_->player()->Next();
|
||||
|
||||
}
|
||||
|
||||
// TODO: Support sort orders.
|
||||
MprisPlaylistList Mpris2::GetPlaylists(quint32 index, quint32 max_count, const QString &order, bool reverse_order) {
|
||||
|
||||
MprisPlaylistList ret;
|
||||
for (Playlist* p : app_->playlist_manager()->GetAllPlaylists()) {
|
||||
MprisPlaylist mpris_playlist;
|
||||
mpris_playlist.id = MakePlaylistPath(p->id());
|
||||
mpris_playlist.name = app_->playlist_manager()->GetPlaylistName(p->id());
|
||||
ret << mpris_playlist;
|
||||
}
|
||||
|
||||
if (reverse_order) {
|
||||
std::reverse(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
return ret.mid(index, max_count);
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::PlaylistChanged(Playlist* playlist) {
|
||||
|
||||
MprisPlaylist mpris_playlist;
|
||||
mpris_playlist.id = MakePlaylistPath(playlist->id());
|
||||
mpris_playlist.name = app_->playlist_manager()->GetPlaylistName(playlist->id());
|
||||
emit PlaylistChanged(mpris_playlist);
|
||||
|
||||
}
|
||||
|
||||
void Mpris2::PlaylistCollectionChanged(Playlist* playlist) {
|
||||
EmitNotification("PlaylistCount", "", "org.mpris.MediaPlayer2.Playlists");
|
||||
}
|
||||
|
||||
} // namespace mpris
|
||||
|
||||
234
src/core/mpris2.h
Normal file
234
src/core/mpris2.h
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MPRIS2_H
|
||||
#define MPRIS2_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "playlist/playlistitem.h"
|
||||
|
||||
#include <QMetaObject>
|
||||
#include <QObject>
|
||||
#include <QtDBus>
|
||||
|
||||
class Application;
|
||||
class MainWindow;
|
||||
class Playlist;
|
||||
|
||||
typedef QList<QVariantMap> TrackMetadata;
|
||||
typedef QList<QDBusObjectPath> TrackIds;
|
||||
Q_DECLARE_METATYPE(TrackMetadata);
|
||||
|
||||
struct MprisPlaylist {
|
||||
QDBusObjectPath id;
|
||||
QString name;
|
||||
QString icon; // Uri
|
||||
};
|
||||
typedef QList<MprisPlaylist> MprisPlaylistList;
|
||||
Q_DECLARE_METATYPE(MprisPlaylist);
|
||||
Q_DECLARE_METATYPE(MprisPlaylistList);
|
||||
|
||||
struct MaybePlaylist {
|
||||
bool valid;
|
||||
MprisPlaylist playlist;
|
||||
};
|
||||
Q_DECLARE_METATYPE(MaybePlaylist);
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &arg, const MprisPlaylist &playlist);
|
||||
const QDBusArgument &operator>> (const QDBusArgument &arg, MprisPlaylist &playlist);
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &arg, const MaybePlaylist &playlist);
|
||||
const QDBusArgument &operator>> (const QDBusArgument &arg, MaybePlaylist &playlist);
|
||||
|
||||
QDBusArgument &operator<< (QDBusArgument &arg, const QImage &image);
|
||||
const QDBusArgument &operator>> (const QDBusArgument &arg, QImage &image);
|
||||
|
||||
namespace mpris {
|
||||
|
||||
class Mpris2 : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Mpris2(Application *app, QObject *parent = nullptr);
|
||||
|
||||
// org.mpris.MediaPlayer2 MPRIS 2.0 Root interface
|
||||
Q_PROPERTY(bool CanQuit READ CanQuit)
|
||||
Q_PROPERTY(bool CanRaise READ CanRaise)
|
||||
Q_PROPERTY(bool HasTrackList READ HasTrackList)
|
||||
Q_PROPERTY(QString Identity READ Identity)
|
||||
Q_PROPERTY(QString DesktopEntry READ DesktopEntry)
|
||||
Q_PROPERTY(QStringList SupportedUriSchemes READ SupportedUriSchemes)
|
||||
Q_PROPERTY(QStringList SupportedMimeTypes READ SupportedMimeTypes)
|
||||
|
||||
// org.mpris.MediaPlayer2 MPRIS 2.2 Root interface
|
||||
Q_PROPERTY(bool CanSetFullscreen READ CanSetFullscreen)
|
||||
Q_PROPERTY(bool Fullscreen READ Fullscreen WRITE SetFullscreen)
|
||||
|
||||
// org.mpris.MediaPlayer2.Player MPRIS 2.0 Player interface
|
||||
Q_PROPERTY(QString PlaybackStatus READ PlaybackStatus)
|
||||
Q_PROPERTY(QString LoopStatus READ LoopStatus WRITE SetLoopStatus)
|
||||
Q_PROPERTY(double Rate READ Rate WRITE SetRate)
|
||||
Q_PROPERTY(bool Shuffle READ Shuffle WRITE SetShuffle)
|
||||
Q_PROPERTY(QVariantMap Metadata READ Metadata)
|
||||
Q_PROPERTY(double Volume READ Volume WRITE SetVolume)
|
||||
Q_PROPERTY(qlonglong Position READ Position)
|
||||
Q_PROPERTY(double MinimumRate READ MinimumRate)
|
||||
Q_PROPERTY(double MaximumRate READ MaximumRate)
|
||||
Q_PROPERTY(bool CanGoNext READ CanGoNext)
|
||||
Q_PROPERTY(bool CanGoPrevious READ CanGoPrevious)
|
||||
Q_PROPERTY(bool CanPlay READ CanPlay)
|
||||
Q_PROPERTY(bool CanPause READ CanPause)
|
||||
Q_PROPERTY(bool CanSeek READ CanSeek)
|
||||
Q_PROPERTY(bool CanControl READ CanControl)
|
||||
|
||||
// org.mpris.MediaPlayer2.TrackList MPRIS 2.0 Player interface
|
||||
Q_PROPERTY(TrackIds Tracks READ Tracks)
|
||||
Q_PROPERTY(bool CanEditTracks READ CanEditTracks)
|
||||
|
||||
// org.mpris.MediaPlayer2.Playlists MPRIS 2.1 Playlists interface
|
||||
Q_PROPERTY(quint32 PlaylistCount READ PlaylistCount)
|
||||
Q_PROPERTY(QStringList Orderings READ Orderings)
|
||||
Q_PROPERTY(MaybePlaylist ActivePlaylist READ ActivePlaylist)
|
||||
|
||||
// Root Properties
|
||||
bool CanQuit() const;
|
||||
bool CanRaise() const;
|
||||
bool HasTrackList() const;
|
||||
QString Identity() const;
|
||||
QString DesktopEntry() const;
|
||||
QStringList SupportedUriSchemes() const;
|
||||
QStringList SupportedMimeTypes() const;
|
||||
|
||||
// Root Properties added in MPRIS 2.2
|
||||
bool CanSetFullscreen() const { return false; }
|
||||
bool Fullscreen() const { return false; }
|
||||
void SetFullscreen(bool) {}
|
||||
|
||||
// Methods
|
||||
void Raise();
|
||||
void Quit();
|
||||
|
||||
// Player Properties
|
||||
QString PlaybackStatus() const;
|
||||
QString LoopStatus() const;
|
||||
void SetLoopStatus(const QString &value);
|
||||
double Rate() const;
|
||||
void SetRate(double value);
|
||||
bool Shuffle() const;
|
||||
void SetShuffle(bool value);
|
||||
QVariantMap Metadata() const;
|
||||
double Volume() const;
|
||||
void SetVolume(double value);
|
||||
qlonglong Position() const;
|
||||
double MaximumRate() const;
|
||||
double MinimumRate() const;
|
||||
bool CanGoNext() const;
|
||||
bool CanGoPrevious() const;
|
||||
bool CanPlay() const;
|
||||
bool CanPause() const;
|
||||
bool CanSeek() const;
|
||||
bool CanControl() const;
|
||||
|
||||
// Methods
|
||||
void Next();
|
||||
void Previous();
|
||||
void Pause();
|
||||
void PlayPause();
|
||||
void Stop();
|
||||
void Play();
|
||||
void Seek(qlonglong offset);
|
||||
void SetPosition(const QDBusObjectPath &trackId, qlonglong offset);
|
||||
void OpenUri(const QString &uri);
|
||||
|
||||
// TrackList Properties
|
||||
TrackIds Tracks() const;
|
||||
bool CanEditTracks() const;
|
||||
|
||||
// Methods
|
||||
TrackMetadata GetTracksMetadata(const TrackIds &tracks) const;
|
||||
void AddTrack(const QString &uri, const QDBusObjectPath &afterTrack, bool setAsCurrent);
|
||||
void RemoveTrack(const QDBusObjectPath &trackId);
|
||||
void GoTo(const QDBusObjectPath &trackId);
|
||||
|
||||
// Playlist Properties
|
||||
quint32 PlaylistCount() const;
|
||||
QStringList Orderings() const;
|
||||
MaybePlaylist ActivePlaylist() const;
|
||||
|
||||
// Methods
|
||||
void ActivatePlaylist(const QDBusObjectPath &playlist_id);
|
||||
QList<MprisPlaylist> GetPlaylists(quint32 index, quint32 max_count, const QString &order, bool reverse_order);
|
||||
|
||||
signals:
|
||||
// Player
|
||||
void Seeked(qlonglong position);
|
||||
|
||||
// TrackList
|
||||
void TrackListReplaced(const TrackIds &Tracks, QDBusObjectPath CurrentTrack);
|
||||
void TrackAdded(const TrackMetadata &Metadata, QDBusObjectPath AfterTrack);
|
||||
void TrackRemoved(const QDBusObjectPath &trackId);
|
||||
void TrackMetadataChanged(const QDBusObjectPath &trackId, const TrackMetadata &metadata);
|
||||
|
||||
void RaiseMainWindow();
|
||||
|
||||
// Playlist
|
||||
void PlaylistChanged(const MprisPlaylist &playlist);
|
||||
|
||||
private slots:
|
||||
void ArtLoaded(const Song &song, const QString &art_uri);
|
||||
void EngineStateChanged(Engine::State newState);
|
||||
void VolumeChanged();
|
||||
|
||||
void PlaylistManagerInitialized();
|
||||
void CurrentSongChanged(const Song &song);
|
||||
void ShuffleModeChanged();
|
||||
void RepeatModeChanged();
|
||||
void PlaylistChanged(Playlist *playlist);
|
||||
void PlaylistCollectionChanged(Playlist *playlist);
|
||||
|
||||
private:
|
||||
void EmitNotification(const QString &name);
|
||||
void EmitNotification(const QString &name, const QVariant &val);
|
||||
void EmitNotification(const QString &name, const QVariant &val, const QString &mprisEntity);
|
||||
|
||||
QString PlaybackStatus(Engine::State state) const;
|
||||
|
||||
QString current_track_id() const;
|
||||
|
||||
bool CanSeek(Engine::State state) const;
|
||||
|
||||
QString DesktopEntryAbsolutePath() const;
|
||||
|
||||
private:
|
||||
static const char *kMprisObjectPath;
|
||||
static const char *kServiceName;
|
||||
static const char *kFreedesktopPath;
|
||||
|
||||
QVariantMap last_metadata_;
|
||||
|
||||
Application *app_;
|
||||
|
||||
};
|
||||
|
||||
} // namespace mpris
|
||||
|
||||
|
||||
#endif
|
||||
64
src/core/mpris_common.h
Normal file
64
src/core/mpris_common.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MPRIS_COMMON_H
|
||||
#define MPRIS_COMMON_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QVariantMap>
|
||||
|
||||
namespace mpris {
|
||||
|
||||
inline void AddMetadata(const QString &key, const QString &metadata, QVariantMap *map) {
|
||||
if (!metadata.isEmpty()) (*map)[key] = metadata;
|
||||
}
|
||||
|
||||
inline void AddMetadataAsList(const QString &key, const QString &metadata, QVariantMap *map) {
|
||||
if (!metadata.isEmpty()) (*map)[key] = QStringList() << metadata;
|
||||
}
|
||||
|
||||
inline void AddMetadata(const QString &key, int metadata, QVariantMap *map) {
|
||||
if (metadata > 0) (*map)[key] = metadata;
|
||||
}
|
||||
|
||||
inline void AddMetadata(const QString &key, qint64 metadata, QVariantMap *map) {
|
||||
if (metadata > 0) (*map)[key] = metadata;
|
||||
}
|
||||
|
||||
inline void AddMetadata(const QString &key, double metadata, QVariantMap *map) {
|
||||
if (metadata != 0.0) (*map)[key] = metadata;
|
||||
}
|
||||
|
||||
inline void AddMetadata(const QString &key, const QDateTime &metadata, QVariantMap *map) {
|
||||
if (metadata.isValid()) (*map)[key] = metadata;
|
||||
}
|
||||
|
||||
inline QString AsMPRISDateTimeType(uint time) {
|
||||
return time != -1 ? QDateTime::fromTime_t(time).toString(Qt::ISODate) : "";
|
||||
}
|
||||
|
||||
} // namespace mpris
|
||||
|
||||
#endif // MPRIS_COMMON_H
|
||||
|
||||
90
src/core/multisortfilterproxy.cpp
Normal file
90
src/core/multisortfilterproxy.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "multisortfilterproxy.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QTime>
|
||||
|
||||
MultiSortFilterProxy::MultiSortFilterProxy(QObject *parent)
|
||||
: QSortFilterProxyModel(parent) {}
|
||||
|
||||
void MultiSortFilterProxy::AddSortSpec(int role, Qt::SortOrder order) {
|
||||
sorting_ << SortSpec(role, order);
|
||||
}
|
||||
|
||||
bool MultiSortFilterProxy::lessThan(const QModelIndex &left, const QModelIndex &right) const {
|
||||
|
||||
for (const SortSpec &spec : sorting_) {
|
||||
const int ret = Compare(left.data(spec.first), right.data(spec.first));
|
||||
|
||||
if (ret < 0) {
|
||||
return spec.second == Qt::AscendingOrder;
|
||||
}
|
||||
else if (ret > 0) {
|
||||
return spec.second != Qt::AscendingOrder;
|
||||
}
|
||||
}
|
||||
|
||||
return left.row() < right.row();
|
||||
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static inline int DoCompare(T left, T right) {
|
||||
|
||||
if (left < right) return -1;
|
||||
if (left > right) return 1;
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
int MultiSortFilterProxy::Compare(const QVariant &left, const QVariant &right) const {
|
||||
|
||||
// Copied from the QSortFilterProxyModel::lessThan implementation, but returns
|
||||
// -1, 0 or 1 instead of true or false.
|
||||
switch (left.userType()) {
|
||||
case QVariant::Invalid: return (right.type() != QVariant::Invalid) ? -1 : 0;
|
||||
case QVariant::Int: return DoCompare(left.toInt(), right.toInt());
|
||||
case QVariant::UInt: return DoCompare(left.toUInt(), right.toUInt());
|
||||
case QVariant::LongLong: return DoCompare(left.toLongLong(), right.toLongLong());
|
||||
case QVariant::ULongLong: return DoCompare(left.toULongLong(), right.toULongLong());
|
||||
case QMetaType::Float: return DoCompare(left.toFloat(), right.toFloat());
|
||||
case QVariant::Double: return DoCompare(left.toDouble(), right.toDouble());
|
||||
case QVariant::Char: return DoCompare(left.toChar(), right.toChar());
|
||||
case QVariant::Date: return DoCompare(left.toDate(), right.toDate());
|
||||
case QVariant::Time: return DoCompare(left.toTime(), right.toTime());
|
||||
case QVariant::DateTime: return DoCompare(left.toDateTime(), right.toDateTime());
|
||||
case QVariant::String:
|
||||
default:
|
||||
if (isSortLocaleAware())
|
||||
return left.toString().localeAwareCompare(right.toString());
|
||||
else
|
||||
return left.toString().compare(right.toString(), sortCaseSensitivity());
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
45
src/core/multisortfilterproxy.h
Normal file
45
src/core/multisortfilterproxy.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MULTISORTFILTERPROXY_H
|
||||
#define MULTISORTFILTERPROXY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class MultiSortFilterProxy : public QSortFilterProxyModel {
|
||||
public:
|
||||
MultiSortFilterProxy(QObject *parent = nullptr);
|
||||
|
||||
void AddSortSpec(int role, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
|
||||
|
||||
private:
|
||||
int Compare(const QVariant &left, const QVariant &right) const;
|
||||
|
||||
typedef QPair<int, Qt::SortOrder> SortSpec;
|
||||
QList<SortSpec> sorting_;
|
||||
};
|
||||
|
||||
#endif // MULTISORTFILTERPROXY_H
|
||||
|
||||
28
src/core/musicstorage.cpp
Normal file
28
src/core/musicstorage.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "musicstorage.h"
|
||||
|
||||
MusicStorage::MusicStorage()
|
||||
{
|
||||
}
|
||||
|
||||
89
src/core/musicstorage.h
Normal file
89
src/core/musicstorage.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 MUSICSTORAGE_H
|
||||
#define MUSICSTORAGE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "song.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
class MusicStorage {
|
||||
public:
|
||||
MusicStorage();
|
||||
virtual ~MusicStorage() {}
|
||||
|
||||
enum Role {
|
||||
Role_Storage = Qt::UserRole + 100,
|
||||
Role_StorageForceConnect,
|
||||
Role_Capacity,
|
||||
Role_FreeSpace,
|
||||
};
|
||||
|
||||
// Values are saved in the database - don't change
|
||||
enum TranscodeMode {
|
||||
Transcode_Always = 1,
|
||||
Transcode_Never = 2,
|
||||
Transcode_Unsupported = 3,
|
||||
};
|
||||
|
||||
typedef std::function<void(float progress)> ProgressFunction;
|
||||
|
||||
struct CopyJob {
|
||||
QString source_;
|
||||
QString destination_;
|
||||
Song metadata_;
|
||||
bool overwrite_;
|
||||
bool mark_as_listened_;
|
||||
bool remove_original_;
|
||||
ProgressFunction progress_;
|
||||
};
|
||||
|
||||
struct DeleteJob {
|
||||
Song metadata_;
|
||||
};
|
||||
|
||||
virtual QString LocalPath() const { return QString(); }
|
||||
|
||||
virtual TranscodeMode GetTranscodeMode() const { return Transcode_Never; }
|
||||
virtual Song::FileType GetTranscodeFormat() const { return Song::Type_Unknown; }
|
||||
virtual bool GetSupportedFiletypes(QList<Song::FileType>* ret) { return true; }
|
||||
|
||||
virtual bool StartCopy(QList<Song::FileType>* supported_types) { return true;}
|
||||
virtual bool CopyToStorage(const CopyJob& job) = 0;
|
||||
virtual void FinishCopy(bool success) {}
|
||||
|
||||
virtual void StartDelete() {}
|
||||
virtual bool DeleteFromStorage(const DeleteJob& job) = 0;
|
||||
virtual void FinishDelete(bool success) {}
|
||||
|
||||
virtual void Eject() {}
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(MusicStorage*);
|
||||
Q_DECLARE_METATYPE(std::shared_ptr<MusicStorage>);
|
||||
|
||||
#endif // MUSICSTORAGE_H
|
||||
|
||||
224
src/core/network.cpp
Normal file
224
src/core/network.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "network.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkDiskCache>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "utilities.h"
|
||||
|
||||
QMutex ThreadSafeNetworkDiskCache::sMutex;
|
||||
QNetworkDiskCache *ThreadSafeNetworkDiskCache::sCache = nullptr;
|
||||
|
||||
ThreadSafeNetworkDiskCache::ThreadSafeNetworkDiskCache(QObject *parent)
|
||||
: QAbstractNetworkCache(parent) {
|
||||
QMutexLocker l(&sMutex);
|
||||
if (!sCache) {
|
||||
sCache = new QNetworkDiskCache;
|
||||
sCache->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_NetworkCache));
|
||||
}
|
||||
}
|
||||
|
||||
qint64 ThreadSafeNetworkDiskCache::cacheSize() const {
|
||||
QMutexLocker l(&sMutex);
|
||||
return sCache->cacheSize();
|
||||
}
|
||||
|
||||
QIODevice *ThreadSafeNetworkDiskCache::data(const QUrl &url) {
|
||||
QMutexLocker l(&sMutex);
|
||||
return sCache->data(url);
|
||||
}
|
||||
|
||||
void ThreadSafeNetworkDiskCache::insert(QIODevice *device) {
|
||||
QMutexLocker l(&sMutex);
|
||||
sCache->insert(device);
|
||||
}
|
||||
|
||||
QNetworkCacheMetaData ThreadSafeNetworkDiskCache::metaData(const QUrl &url) {
|
||||
QMutexLocker l(&sMutex);
|
||||
return sCache->metaData(url);
|
||||
}
|
||||
|
||||
QIODevice *ThreadSafeNetworkDiskCache::prepare(const QNetworkCacheMetaData &metaData) {
|
||||
QMutexLocker l(&sMutex);
|
||||
return sCache->prepare(metaData);
|
||||
}
|
||||
|
||||
bool ThreadSafeNetworkDiskCache::remove(const QUrl &url) {
|
||||
QMutexLocker l(&sMutex);
|
||||
return sCache->remove(url);
|
||||
}
|
||||
|
||||
void ThreadSafeNetworkDiskCache::updateMetaData(const QNetworkCacheMetaData &metaData) {
|
||||
QMutexLocker l(&sMutex);
|
||||
sCache->updateMetaData(metaData);
|
||||
}
|
||||
|
||||
void ThreadSafeNetworkDiskCache::clear() {
|
||||
QMutexLocker l(&sMutex);
|
||||
sCache->clear();
|
||||
}
|
||||
|
||||
NetworkAccessManager::NetworkAccessManager(QObject *parent)
|
||||
: QNetworkAccessManager(parent) {
|
||||
setCache(new ThreadSafeNetworkDiskCache(this));
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkAccessManager::createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) {
|
||||
|
||||
QByteArray user_agent = QString("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()).toUtf8();
|
||||
|
||||
if (request.hasRawHeader("User-Agent")) {
|
||||
// Append the existing user-agent set by a client collection (such as
|
||||
// libmygpo-qt).
|
||||
user_agent += " " + request.rawHeader("User-Agent");
|
||||
}
|
||||
|
||||
QNetworkRequest new_request(request);
|
||||
new_request.setRawHeader("User-Agent", user_agent);
|
||||
|
||||
if (op == QNetworkAccessManager::PostOperation &&
|
||||
!new_request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
new_request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
"application/x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
// Prefer the cache unless the caller has changed the setting already
|
||||
if (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt() == QNetworkRequest::PreferNetwork) {
|
||||
new_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
|
||||
}
|
||||
|
||||
return QNetworkAccessManager::createRequest(op, new_request, outgoingData);
|
||||
}
|
||||
|
||||
|
||||
NetworkTimeouts::NetworkTimeouts(int timeout_msec, QObject *parent)
|
||||
: QObject(parent), timeout_msec_(timeout_msec) {}
|
||||
|
||||
void NetworkTimeouts::AddReply(QNetworkReply *reply) {
|
||||
|
||||
if (timers_.contains(reply)) return;
|
||||
|
||||
connect(reply, SIGNAL(destroyed()), SLOT(ReplyFinished()));
|
||||
connect(reply, SIGNAL(finished()), SLOT(ReplyFinished()));
|
||||
timers_[reply] = startTimer(timeout_msec_);
|
||||
|
||||
}
|
||||
|
||||
void NetworkTimeouts::AddReply(RedirectFollower *reply) {
|
||||
|
||||
if (redirect_timers_.contains(reply)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NewClosure(reply, SIGNAL(destroyed()), this, SLOT(RedirectFinished(RedirectFollower*)), reply);
|
||||
NewClosure(reply, SIGNAL(finished()), this, SLOT(RedirectFinished(RedirectFollower*)), reply);
|
||||
redirect_timers_[reply] = startTimer(timeout_msec_);
|
||||
|
||||
}
|
||||
|
||||
void NetworkTimeouts::ReplyFinished() {
|
||||
|
||||
QNetworkReply *reply = reinterpret_cast<QNetworkReply*>(sender());
|
||||
if (timers_.contains(reply)) {
|
||||
killTimer(timers_.take(reply));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void NetworkTimeouts::RedirectFinished(RedirectFollower *reply) {
|
||||
|
||||
if (redirect_timers_.contains(reply)) {
|
||||
killTimer(redirect_timers_.take(reply));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void NetworkTimeouts::timerEvent(QTimerEvent *e) {
|
||||
|
||||
QNetworkReply *reply = timers_.key(e->timerId());
|
||||
if (reply) {
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
RedirectFollower *redirect = redirect_timers_.key(e->timerId());
|
||||
if (redirect) {
|
||||
redirect->abort();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
RedirectFollower::RedirectFollower(QNetworkReply *first_reply, int max_redirects) : QObject(nullptr), current_reply_(first_reply), redirects_remaining_(max_redirects) {
|
||||
ConnectReply(first_reply);
|
||||
}
|
||||
|
||||
void RedirectFollower::ConnectReply(QNetworkReply *reply) {
|
||||
|
||||
connect(reply, SIGNAL(readyRead()), SLOT(ReadyRead()));
|
||||
connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SIGNAL(error(QNetworkReply::NetworkError)));
|
||||
connect(reply, SIGNAL(downloadProgress(qint64,qint64)), SIGNAL(downloadProgress(qint64,qint64)));
|
||||
connect(reply, SIGNAL(uploadProgress(qint64,qint64)), SIGNAL(uploadProgress(qint64,qint64)));
|
||||
connect(reply, SIGNAL(finished()), SLOT(ReplyFinished()));
|
||||
|
||||
}
|
||||
|
||||
void RedirectFollower::ReadyRead() {
|
||||
|
||||
// Don't re-emit this signal for redirect replies.
|
||||
if (current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit readyRead();
|
||||
|
||||
}
|
||||
|
||||
void RedirectFollower::ReplyFinished() {
|
||||
|
||||
current_reply_->deleteLater();
|
||||
|
||||
if (current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
|
||||
if (redirects_remaining_-- == 0) {
|
||||
emit finished();
|
||||
return;
|
||||
}
|
||||
|
||||
const QUrl next_url = current_reply_->url().resolved(current_reply_->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
|
||||
|
||||
QNetworkRequest req(current_reply_->request());
|
||||
req.setUrl(next_url);
|
||||
|
||||
current_reply_ = current_reply_->manager()->get(req);
|
||||
ConnectReply(current_reply_);
|
||||
return;
|
||||
}
|
||||
|
||||
emit finished();
|
||||
|
||||
}
|
||||
|
||||
128
src/core/network.h
Normal file
128
src/core/network.h
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 NETWORK_H
|
||||
#define NETWORK_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAbstractNetworkCache>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class QNetworkDiskCache;
|
||||
|
||||
class ThreadSafeNetworkDiskCache : public QAbstractNetworkCache {
|
||||
public:
|
||||
explicit ThreadSafeNetworkDiskCache(QObject *parent);
|
||||
|
||||
qint64 cacheSize() const;
|
||||
QIODevice *data(const QUrl &url);
|
||||
void insert(QIODevice *device);
|
||||
QNetworkCacheMetaData metaData(const QUrl &url);
|
||||
QIODevice *prepare(const QNetworkCacheMetaData &metaData);
|
||||
bool remove(const QUrl &url);
|
||||
void updateMetaData(const QNetworkCacheMetaData &metaData);
|
||||
|
||||
void clear();
|
||||
|
||||
private:
|
||||
static QMutex sMutex;
|
||||
static QNetworkDiskCache *sCache;
|
||||
};
|
||||
|
||||
class NetworkAccessManager : public QNetworkAccessManager {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NetworkAccessManager(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData);
|
||||
};
|
||||
|
||||
class RedirectFollower : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RedirectFollower(QNetworkReply *first_reply, int max_redirects = 5);
|
||||
|
||||
bool hit_redirect_limit() const { return redirects_remaining_ < 0; }
|
||||
QNetworkReply *reply() const { return current_reply_; }
|
||||
|
||||
// These are all forwarded to the current reply.
|
||||
QNetworkReply::NetworkError error() const { return current_reply_->error(); }
|
||||
QString errorString() const { return current_reply_->errorString(); }
|
||||
QVariant attribute(QNetworkRequest::Attribute code) const { return current_reply_->attribute(code); }
|
||||
QVariant header(QNetworkRequest::KnownHeaders header) const { return current_reply_->header(header); }
|
||||
qint64 bytesAvailable() const { return current_reply_->bytesAvailable(); }
|
||||
QUrl url() const { return current_reply_->url(); }
|
||||
QByteArray readAll() { return current_reply_->readAll(); }
|
||||
void abort() { current_reply_->abort(); }
|
||||
|
||||
signals:
|
||||
// These are all forwarded from the current reply.
|
||||
void readyRead();
|
||||
void error(QNetworkReply::NetworkError);
|
||||
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
|
||||
// This is NOT emitted when a request that has a redirect finishes.
|
||||
void finished();
|
||||
|
||||
private slots:
|
||||
void ReadyRead();
|
||||
void ReplyFinished();
|
||||
|
||||
private:
|
||||
void ConnectReply(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QNetworkReply *current_reply_;
|
||||
int redirects_remaining_;
|
||||
};
|
||||
|
||||
class NetworkTimeouts : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NetworkTimeouts(int timeout_msec, QObject *parent = nullptr);
|
||||
|
||||
// TODO: Template this to avoid code duplication.
|
||||
void AddReply(QNetworkReply *reply);
|
||||
void AddReply(RedirectFollower *reply);
|
||||
void SetTimeout(int msec) { timeout_msec_ = msec; }
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent *e);
|
||||
|
||||
private slots:
|
||||
void ReplyFinished();
|
||||
void RedirectFinished(RedirectFollower *redirect);
|
||||
|
||||
private:
|
||||
int timeout_msec_;
|
||||
QMap<QNetworkReply*, int> timers_;
|
||||
QMap<RedirectFollower*, int> redirect_timers_;
|
||||
};
|
||||
|
||||
#endif // NETWORK_H
|
||||
|
||||
142
src/core/networkproxyfactory.cpp
Normal file
142
src/core/networkproxyfactory.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "networkproxyfactory.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <QMutexLocker>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
|
||||
NetworkProxyFactory *NetworkProxyFactory::sInstance = nullptr;
|
||||
const char *NetworkProxyFactory::kSettingsGroup = "NetworkProxy";
|
||||
|
||||
NetworkProxyFactory::NetworkProxyFactory()
|
||||
: mode_(Mode_System),
|
||||
type_(QNetworkProxy::HttpProxy),
|
||||
port_(8080),
|
||||
use_authentication_(false) {
|
||||
#ifdef Q_OS_LINUX
|
||||
// Linux uses environment variables to pass proxy configuration information,
|
||||
// which systemProxyForQuery doesn't support for some reason.
|
||||
|
||||
QStringList urls;
|
||||
urls << QString::fromLocal8Bit(getenv("HTTP_PROXY"));
|
||||
urls << QString::fromLocal8Bit(getenv("http_proxy"));
|
||||
urls << QString::fromLocal8Bit(getenv("ALL_PROXY"));
|
||||
urls << QString::fromLocal8Bit(getenv("all_proxy"));
|
||||
|
||||
qLog(Debug) << "Detected system proxy URLs:" << urls;
|
||||
|
||||
for (const QString &url_str : urls) {
|
||||
|
||||
if (url_str.isEmpty()) continue;
|
||||
env_url_ = QUrl(url_str);
|
||||
break;
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
NetworkProxyFactory *NetworkProxyFactory::Instance() {
|
||||
|
||||
if (!sInstance) {
|
||||
sInstance = new NetworkProxyFactory;
|
||||
}
|
||||
|
||||
return sInstance;
|
||||
|
||||
}
|
||||
|
||||
void NetworkProxyFactory::ReloadSettings() {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
mode_ = Mode(s.value("mode", Mode_System).toInt());
|
||||
type_ = QNetworkProxy::ProxyType(s.value("type", QNetworkProxy::HttpProxy).toInt());
|
||||
hostname_ = s.value("hostname").toString();
|
||||
port_ = s.value("port", 8080).toInt();
|
||||
use_authentication_ = s.value("use_authentication", false).toBool();
|
||||
username_ = s.value("username").toString();
|
||||
password_ = s.value("password").toString();
|
||||
|
||||
}
|
||||
|
||||
QList<QNetworkProxy> NetworkProxyFactory::queryProxy(const QNetworkProxyQuery &query) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
|
||||
QNetworkProxy ret;
|
||||
|
||||
switch (mode_) {
|
||||
case Mode_System:
|
||||
#ifdef Q_OS_LINUX
|
||||
Q_UNUSED(query);
|
||||
|
||||
if (env_url_.isEmpty()) {
|
||||
ret.setType(QNetworkProxy::NoProxy);
|
||||
}
|
||||
else {
|
||||
ret.setHostName(env_url_.host());
|
||||
ret.setPort(env_url_.port());
|
||||
ret.setUser(env_url_.userName());
|
||||
ret.setPassword(env_url_.password());
|
||||
if (env_url_.scheme().startsWith("http"))
|
||||
ret.setType(QNetworkProxy::HttpProxy);
|
||||
else
|
||||
ret.setType(QNetworkProxy::Socks5Proxy);
|
||||
qLog(Debug) << "Using proxy URL:" << env_url_;
|
||||
}
|
||||
break;
|
||||
#else
|
||||
return systemProxyForQuery(query);
|
||||
#endif
|
||||
|
||||
case Mode_Direct:
|
||||
ret.setType(QNetworkProxy::NoProxy);
|
||||
break;
|
||||
|
||||
case Mode_Manual:
|
||||
ret.setType(type_);
|
||||
ret.setHostName(hostname_);
|
||||
ret.setPort(port_);
|
||||
if (use_authentication_) {
|
||||
ret.setUser(username_);
|
||||
ret.setPassword(password_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return QList<QNetworkProxy>() << ret;
|
||||
|
||||
}
|
||||
|
||||
62
src/core/networkproxyfactory.h
Normal file
62
src/core/networkproxyfactory.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 NETWORKPROXYFACTORY_H
|
||||
#define NETWORKPROXYFACTORY_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QUrl>
|
||||
|
||||
class NetworkProxyFactory : public QNetworkProxyFactory {
|
||||
public:
|
||||
// These values are persisted
|
||||
enum Mode { Mode_System = 0, Mode_Direct = 1, Mode_Manual = 2, };
|
||||
|
||||
static NetworkProxyFactory *Instance();
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
// These methods are thread-safe
|
||||
void ReloadSettings();
|
||||
QList<QNetworkProxy> queryProxy(const QNetworkProxyQuery &query);
|
||||
|
||||
private:
|
||||
NetworkProxyFactory();
|
||||
|
||||
static NetworkProxyFactory *sInstance;
|
||||
|
||||
QMutex mutex_;
|
||||
|
||||
Mode mode_;
|
||||
QNetworkProxy::ProxyType type_;
|
||||
QString hostname_;
|
||||
int port_;
|
||||
bool use_authentication_;
|
||||
QString username_;
|
||||
QString password_;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
QUrl env_url_;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // NETWORKPROXYFACTORY_H
|
||||
298
src/core/organise.cpp
Normal file
298
src/core/organise.cpp
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 "organise.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#include <QUrl>
|
||||
|
||||
#include "musicstorage.h"
|
||||
#include "taskmanager.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
using std::placeholders::_1;
|
||||
|
||||
const int Organise::kBatchSize = 10;
|
||||
const int Organise::kTranscodeProgressInterval = 500;
|
||||
|
||||
Organise::Organise(TaskManager *task_manager, std::shared_ptr<MusicStorage> destination, const OrganiseFormat &format, bool copy, bool overwrite, bool mark_as_listened, const NewSongInfoList &songs_info, bool eject_after)
|
||||
: thread_(nullptr),
|
||||
task_manager_(task_manager),
|
||||
transcoder_(new Transcoder(this)),
|
||||
destination_(destination),
|
||||
format_(format),
|
||||
copy_(copy),
|
||||
overwrite_(overwrite),
|
||||
mark_as_listened_(mark_as_listened),
|
||||
eject_after_(eject_after),
|
||||
task_count_(songs_info.count()),
|
||||
transcode_suffix_(1),
|
||||
tasks_complete_(0),
|
||||
started_(false),
|
||||
task_id_(0),
|
||||
current_copy_progress_(0) {
|
||||
|
||||
original_thread_ = thread();
|
||||
|
||||
for (const NewSongInfo &song_info : songs_info) {
|
||||
tasks_pending_ << Task(song_info);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Organise::Start() {
|
||||
|
||||
if (thread_) return;
|
||||
|
||||
task_id_ = task_manager_->StartTask(tr("Organising files"));
|
||||
task_manager_->SetTaskBlocksCollectionScans(true);
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ProcessSomeFiles()));
|
||||
connect(transcoder_, SIGNAL(JobComplete(QString, QString, bool)), SLOT(FileTranscoded(QString, QString, bool)));
|
||||
|
||||
moveToThread(thread_);
|
||||
thread_->start();
|
||||
}
|
||||
|
||||
void Organise::ProcessSomeFiles() {
|
||||
|
||||
if (!started_) {
|
||||
transcode_temp_name_.open();
|
||||
|
||||
if (!destination_->StartCopy(&supported_filetypes_)) {
|
||||
// Failed to start - mark everything as failed :(
|
||||
for (const Task &task : tasks_pending_) files_with_errors_ << task.song_info_.song_.url().toLocalFile();
|
||||
tasks_pending_.clear();
|
||||
}
|
||||
started_ = true;
|
||||
}
|
||||
|
||||
// None left?
|
||||
if (tasks_pending_.isEmpty()) {
|
||||
if (!tasks_transcoding_.isEmpty()) {
|
||||
// Just wait - FileTranscoded will start us off again in a little while
|
||||
qLog(Debug) << "Waiting for transcoding jobs";
|
||||
transcode_progress_timer_.start(kTranscodeProgressInterval, this);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProgress();
|
||||
|
||||
destination_->FinishCopy(files_with_errors_.isEmpty());
|
||||
if (eject_after_) destination_->Eject();
|
||||
|
||||
task_manager_->SetTaskFinished(task_id_);
|
||||
|
||||
emit Finished(files_with_errors_);
|
||||
|
||||
// Move back to the original thread so deleteLater() can get called in
|
||||
// the main thread's event loop
|
||||
moveToThread(original_thread_);
|
||||
deleteLater();
|
||||
|
||||
// Stop this thread
|
||||
thread_->quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// We process files in batches so we can be cancelled part-way through.
|
||||
for (int i = 0; i < kBatchSize; ++i) {
|
||||
SetSongProgress(0);
|
||||
|
||||
if (tasks_pending_.isEmpty()) break;
|
||||
|
||||
Task task = tasks_pending_.takeFirst();
|
||||
qLog(Info) << "Processing" << task.song_info_.song_.url().toLocalFile();
|
||||
|
||||
// Use a Song instead of a tag reader
|
||||
Song song = task.song_info_.song_;
|
||||
if (!song.is_valid()) continue;
|
||||
|
||||
// Maybe this file is one that's been transcoded already?
|
||||
if (!task.transcoded_filename_.isEmpty()) {
|
||||
qLog(Debug) << "This file has already been transcoded";
|
||||
|
||||
// Set the new filetype on the song so the formatter gets it right
|
||||
song.set_filetype(task.new_filetype_);
|
||||
|
||||
// Fiddle the filename extension as well to match the new type
|
||||
song.set_url(QUrl::fromLocalFile(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_)));
|
||||
song.set_basefilename(Utilities::FiddleFileExtension(song.basefilename(), task.new_extension_));
|
||||
|
||||
// Have to set this to the size of the new file or else funny stuff happens
|
||||
song.set_filesize(QFileInfo(task.transcoded_filename_).size());
|
||||
}
|
||||
else {
|
||||
// Figure out if we need to transcode it
|
||||
Song::FileType dest_type = CheckTranscode(song.filetype());
|
||||
if (dest_type != Song::Type_Unknown) {
|
||||
// Get the preset
|
||||
TranscoderPreset preset = Transcoder::PresetForFileType(dest_type);
|
||||
qLog(Debug) << "Transcoding with" << preset.name_;
|
||||
|
||||
// Get a temporary name for the transcoded file
|
||||
task.transcoded_filename_ = transcode_temp_name_.fileName() + "-" + QString::number(transcode_suffix_++);
|
||||
task.new_extension_ = preset.extension_;
|
||||
task.new_filetype_ = dest_type;
|
||||
tasks_transcoding_[task.song_info_.song_.url().toLocalFile()] = task;
|
||||
|
||||
qLog(Debug) << "Transcoding to" << task.transcoded_filename_;
|
||||
|
||||
// Start the transcoding - this will happen in the background and
|
||||
// FileTranscoded() will get called when it's done. At that point the
|
||||
// task will get re-added to the pending queue with the new filename.
|
||||
transcoder_->AddJob(task.song_info_.song_.url().toLocalFile(), preset, task.transcoded_filename_);
|
||||
transcoder_->Start();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
MusicStorage::CopyJob job;
|
||||
job.source_ = task.transcoded_filename_.isEmpty() ? task.song_info_.song_.url().toLocalFile() : task.transcoded_filename_;
|
||||
job.destination_ = task.song_info_.new_filename_;
|
||||
job.metadata_ = song;
|
||||
job.overwrite_ = overwrite_;
|
||||
job.mark_as_listened_ = mark_as_listened_;
|
||||
job.remove_original_ = !copy_;
|
||||
job.progress_ = std::bind(&Organise::SetSongProgress, this, _1, !task.transcoded_filename_.isEmpty());
|
||||
|
||||
if (!destination_->CopyToStorage(job)) {
|
||||
files_with_errors_ << task.song_info_.song_.basefilename();
|
||||
} else {
|
||||
if (job.mark_as_listened_) {
|
||||
emit FileCopied(job.metadata_.id());
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the temporary transcoded file
|
||||
if (!task.transcoded_filename_.isEmpty())
|
||||
QFile::remove(task.transcoded_filename_);
|
||||
|
||||
tasks_complete_++;
|
||||
}
|
||||
SetSongProgress(0);
|
||||
|
||||
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
||||
|
||||
}
|
||||
|
||||
Song::FileType Organise::CheckTranscode(Song::FileType original_type) const {
|
||||
|
||||
//if (original_type == Song::Type_Stream) return Song::Type_Unknown;
|
||||
|
||||
const MusicStorage::TranscodeMode mode = destination_->GetTranscodeMode();
|
||||
const Song::FileType format = destination_->GetTranscodeFormat();
|
||||
|
||||
switch (mode) {
|
||||
case MusicStorage::Transcode_Never:
|
||||
return Song::Type_Unknown;
|
||||
|
||||
case MusicStorage::Transcode_Always:
|
||||
if (original_type == format) return Song::Type_Unknown;
|
||||
return format;
|
||||
|
||||
case MusicStorage::Transcode_Unsupported:
|
||||
if (supported_filetypes_.isEmpty() || supported_filetypes_.contains(original_type)) return Song::Type_Unknown;
|
||||
|
||||
if (format != Song::Type_Unknown) return format;
|
||||
|
||||
// The user hasn't visited the device properties page yet to set a
|
||||
// preferred format for the device, so we have to pick the best
|
||||
// available one.
|
||||
return Transcoder::PickBestFormat(supported_filetypes_);
|
||||
}
|
||||
return Song::Type_Unknown;
|
||||
|
||||
}
|
||||
|
||||
void Organise::SetSongProgress(float progress, bool transcoded) {
|
||||
|
||||
const int max = transcoded ? 50 : 100;
|
||||
current_copy_progress_ = (transcoded ? 50 : 0) + qBound(0, static_cast<int>(progress * max), max - 1);
|
||||
UpdateProgress();
|
||||
|
||||
}
|
||||
|
||||
void Organise::UpdateProgress() {
|
||||
|
||||
const int total = task_count_ * 100;
|
||||
|
||||
// Update transcoding progress
|
||||
QMap<QString, float> transcode_progress = transcoder_->GetProgress();
|
||||
for (const QString &filename : transcode_progress.keys()) {
|
||||
if (!tasks_transcoding_.contains(filename)) continue;
|
||||
tasks_transcoding_[filename].transcode_progress_ = transcode_progress[filename];
|
||||
}
|
||||
|
||||
// Count the progress of all tasks that are in the queue. Files that need
|
||||
// transcoding total 50 for the transcode and 50 for the copy, files that
|
||||
// only need to be copied total 100.
|
||||
int progress = tasks_complete_ * 100;
|
||||
|
||||
for (const Task &task : tasks_pending_) {
|
||||
progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
|
||||
}
|
||||
for (const Task &task : tasks_transcoding_.values()) {
|
||||
progress += qBound(0, static_cast<int>(task.transcode_progress_ * 50), 50);
|
||||
}
|
||||
|
||||
// Add the progress of the track that's currently copying
|
||||
progress += current_copy_progress_;
|
||||
|
||||
task_manager_->SetTaskProgress(task_id_, progress, total);
|
||||
|
||||
}
|
||||
|
||||
void Organise::FileTranscoded(const QString &input, const QString &output, bool success) {
|
||||
|
||||
qLog(Info) << "File finished" << input << success;
|
||||
transcode_progress_timer_.stop();
|
||||
|
||||
Task task = tasks_transcoding_.take(input);
|
||||
if (!success) {
|
||||
files_with_errors_ << input;
|
||||
}
|
||||
else {
|
||||
tasks_pending_ << task;
|
||||
}
|
||||
QTimer::singleShot(0, this, SLOT(ProcessSomeFiles()));
|
||||
|
||||
}
|
||||
|
||||
void Organise::timerEvent(QTimerEvent *e) {
|
||||
|
||||
QObject::timerEvent(e);
|
||||
|
||||
if (e->timerId() == transcode_progress_timer_.timerId()) {
|
||||
UpdateProgress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
114
src/core/organise.h
Normal file
114
src/core/organise.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, 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 ORGANISE_H
|
||||
#define ORGANISE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QBasicTimer>
|
||||
#include <QObject>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
#include "organiseformat.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
class MusicStorage;
|
||||
class TaskManager;
|
||||
|
||||
class Organise : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct NewSongInfo {
|
||||
NewSongInfo(const Song &song = Song(), const QString &new_filename = QString()) : song_(song), new_filename_(new_filename) {}
|
||||
Song song_;
|
||||
QString new_filename_;
|
||||
};
|
||||
typedef QList<NewSongInfo> NewSongInfoList;
|
||||
|
||||
Organise(TaskManager *task_manager, std::shared_ptr<MusicStorage> destination, const OrganiseFormat &format, bool copy, bool overwrite, bool mark_as_listened, const NewSongInfoList &songs, bool eject_after);
|
||||
|
||||
static const int kBatchSize;
|
||||
static const int kTranscodeProgressInterval;
|
||||
|
||||
void Start();
|
||||
|
||||
signals:
|
||||
void Finished(const QStringList &files_with_errors);
|
||||
void FileCopied(int database_id);
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent *e);
|
||||
|
||||
private slots:
|
||||
void ProcessSomeFiles();
|
||||
void FileTranscoded(const QString &input, const QString &output, bool success);
|
||||
|
||||
private:
|
||||
void SetSongProgress(float progress, bool transcoded = false);
|
||||
void UpdateProgress();
|
||||
Song::FileType CheckTranscode(Song::FileType original_type) const;
|
||||
|
||||
private:
|
||||
struct Task {
|
||||
explicit Task(const NewSongInfo &song_info = NewSongInfo()) : song_info_(song_info), transcode_progress_(0.0) {}
|
||||
|
||||
NewSongInfo song_info_;
|
||||
|
||||
float transcode_progress_;
|
||||
QString transcoded_filename_;
|
||||
QString new_extension_;
|
||||
Song::FileType new_filetype_;
|
||||
};
|
||||
|
||||
QThread *thread_;
|
||||
QThread *original_thread_;
|
||||
TaskManager *task_manager_;
|
||||
Transcoder *transcoder_;
|
||||
std::shared_ptr<MusicStorage> destination_;
|
||||
QList<Song::FileType> supported_filetypes_;
|
||||
|
||||
const OrganiseFormat format_;
|
||||
const bool copy_;
|
||||
const bool overwrite_;
|
||||
const bool mark_as_listened_;
|
||||
const bool eject_after_;
|
||||
int task_count_;
|
||||
|
||||
QBasicTimer transcode_progress_timer_;
|
||||
QTemporaryFile transcode_temp_name_;
|
||||
int transcode_suffix_;
|
||||
|
||||
QList<Task> tasks_pending_;
|
||||
QMap<QString, Task> tasks_transcoding_;
|
||||
int tasks_complete_;
|
||||
|
||||
bool started_;
|
||||
|
||||
int task_id_;
|
||||
int current_copy_progress_;
|
||||
|
||||
QStringList files_with_errors_;
|
||||
};
|
||||
|
||||
#endif // ORGANISE_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user