Move everything to trunk

This commit is contained in:
David Sansome 2009-12-24 19:16:07 +00:00
commit 5b0496bf8f
104 changed files with 10533 additions and 0 deletions

13
TODO Normal file
View File

@ -0,0 +1,13 @@
- Crossfading
- Drag & drop from files tab
- Double click folders in files tab
- Nice error messages
- Automatically install xine plugins
Long-term:
- Last.fm
- iPod
Windows:
- Playlist delegates
- Document build steps

BIN
data/album.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
data/artist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

BIN
data/clear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

BIN
data/configure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

BIN
data/currenttrack_pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 B

BIN
data/currenttrack_play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

35
data/data.qrc Normal file
View File

@ -0,0 +1,35 @@
<RCC>
<qresource prefix="/">
<file>clear.png</file>
<file>go-home.png</file>
<file>go-next.png</file>
<file>go-previous.png</file>
<file>go-up.png</file>
<file>media-playback-pause.png</file>
<file>media-playback-start.png</file>
<file>media-playback-stop.png</file>
<file>media-skip-backward.png</file>
<file>media-skip-forward.png</file>
<file>mainwindow.css</file>
<file>schema.sql</file>
<file>volumeslider-handle_glow.png</file>
<file>volumeslider-handle.png</file>
<file>volumeslider-inset.png</file>
<file>volumeslider-gradient.png</file>
<file>logo.png</file>
<file>icon.png</file>
<file>currenttrack_bar_left.png</file>
<file>currenttrack_bar_mid.png</file>
<file>currenttrack_bar_right.png</file>
<file>currenttrack_pause.png</file>
<file>currenttrack_play.png</file>
<file>album.png</file>
<file>artist.png</file>
<file>files.png</file>
<file>nomusic.png</file>
<file>folder-new.png</file>
<file>folder.png</file>
<file>configure.png</file>
<file>exit.png</file>
</qresource>
</RCC>

BIN
data/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
data/files.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

BIN
data/folder-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

BIN
data/folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

BIN
data/go-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

BIN
data/go-next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

BIN
data/go-previous.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

BIN
data/go-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

BIN
data/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
data/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
data/logo.xcf Normal file

Binary file not shown.

8
data/mainwindow.css Normal file
View File

@ -0,0 +1,8 @@
#playlist {
background-color: white;
background-image: url(:logo.png);
background-attachment: fixed;
background-position: bottom right;
background-repeat: none;
background-clip: content;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

BIN
data/media-skip-forward.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
data/nomusic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

32
data/schema.sql Normal file
View File

@ -0,0 +1,32 @@
CREATE TABLE directories (
path TEXT NOT NULL,
subdirs INTEGER NOT NULL
);
CREATE TABLE songs (
/* Metadata from taglib */
title TEXT,
album TEXT,
artist TEXT,
albumartist TEXT,
composer TEXT,
track INTEGER,
disc INTEGER,
bpm REAL,
year INTEGER,
genre TEXT,
comment TEXT,
compilation INTEGER,
length INTEGER,
bitrate INTEGER,
samplerate INTEGER,
/* Information about the file on disk */
directory INTEGER NOT NULL,
filename TEXT NOT NULL,
mtime INTEGER NOT NULL,
ctime INTEGER NOT NULL,
filesize INTEGER NOT NULL
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
data/volumeslider-inset.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

View File

@ -0,0 +1,219 @@
/***************************************************************************
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 "enginebase.h"
#include <cmath> //interpolate()
#include <QEvent> //event()
#include <QPainter>
#include <QPaintEvent>
#include <QtDebug>
// 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 timeout, uint scopeSize )
: QWidget( parent )
, m_timeout( timeout )
, m_fht( new FHT(scopeSize) )
, m_engine(NULL)
, m_lastScope(512)
{
connect( &m_timer, SIGNAL( timeout() ), SLOT( update() ) );
}
void Analyzer::Base::hideEvent(QHideEvent *) {
m_timer.stop();
}
void Analyzer::Base::showEvent(QShowEvent *) {
m_timer.start(timeout());
}
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)
{
EngineBase *engine = m_engine;
QPainter p(this);
p.fillRect(e->rect(), palette().color(QPalette::Window));
switch( engine->state() )
{
case Engine::Playing:
{
const Engine::Scope &thescope = engine->scope();
int i = 0;
// convert to mono here - our built in analyzers need mono, but we 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;
}
transform( m_lastScope );
analyze( p, m_lastScope );
//scope.resize( m_fht->size() );
break;
}
case Engine::Paused:
analyze(p, m_lastScope);
break;
default:
demo(p);
}
}
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::paused(QPainter& p) //virtual
{}
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 );
}
else analyze( p, Scope( 32, 0 ) );
++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;
}
}

View File

@ -0,0 +1,84 @@
// 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 "engine_fwd.h"
#include "fht.h" //stack allocated and convenience
#include <QPixmap> //stack allocated and convenience
#include <QTimer> //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
{
public:
uint timeout() const { return m_timeout; }
void set_engine(EngineBase* engine) { m_engine = engine; }
protected:
Base( QWidget*, uint timeout, uint scopeSize = 7 );
~Base() { delete m_fht; }
void hideEvent(QHideEvent *);
void showEvent(QShowEvent *);
void paintEvent( QPaintEvent* );
void polishEvent();
int resizeExponent( int );
int resizeForBands( int );
virtual void init() {}
virtual void transform( Scope& );
virtual void analyze( QPainter& p, const Scope& ) = 0;
virtual void paused(QPainter& p);
virtual void demo(QPainter& p);
void changeTimeout( uint newTimeout )
{
m_timer.setInterval( newTimeout );
m_timeout = newTimeout;
}
protected:
QTimer m_timer;
uint m_timeout;
FHT *m_fht;
EngineBase* m_engine;
Scope m_lastScope;
};
void interpolate( const Scope&, Scope& );
void initSin( Scope&, const uint = 6000 );
} //END namespace Analyzer
using Analyzer::Scope;
#endif

View File

@ -0,0 +1,127 @@
/***************************************************************************
analyzerfactory.cpp - description
-------------------
begin : Fre Nov 15 2002
copyright : (C) 2002 by Mark Kretschmann
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 <config.h> //for HAVE_QGLWIDGET macro
#include "amarokcore/amarokconfig.h"
#include "analyzerbase.h" //declaration here
#include "baranalyzer.h"
#include "boomanalyzer.h"
#include "sonogram.h"
#include "turbine.h"
#include "blockanalyzer.h"
#ifdef HAVE_QGLWIDGET
#include "glanalyzer.h"
#include "glanalyzer2.h"
#include "glanalyzer3.h"
#endif
#include <qlabel.h>
#include <klocale.h>
//separate from analyzerbase.cpp to save compile time
QWidget *Analyzer::Factory::createAnalyzer( QWidget *parent )
{
//new XmmsWrapper(); //toplevel
QWidget *analyzer = 0;
switch( AmarokConfig::currentAnalyzer() )
{
case 2:
analyzer = new Sonogram( parent );
break;
case 1:
analyzer = new TurbineAnalyzer( parent );
break;
case 3:
analyzer = new BarAnalyzer( parent );
break;
case 4:
analyzer = new BlockAnalyzer( parent );
break;
#ifdef HAVE_QGLWIDGET
case 5:
analyzer = new GLAnalyzer( parent );
break;
case 6:
analyzer = new GLAnalyzer2( parent );
break;
case 7:
analyzer = new GLAnalyzer3( parent );
break;
case 8:
#else
case 5:
#endif
analyzer = new QLabel( i18n( "Click for Analyzers" ), parent ); //blank analyzer to satisfy Grue
static_cast<QLabel *>(analyzer)->setAlignment( Qt::AlignCenter );
break;
default:
AmarokConfig::setCurrentAnalyzer( 0 );
case 0:
analyzer = new BoomAnalyzer( parent );
}
return analyzer;
}
QWidget *Analyzer::Factory::createPlaylistAnalyzer( QWidget *parent)
{
QWidget *analyzer = 0;
switch( AmarokConfig::currentPlaylistAnalyzer() )
{
case 1:
analyzer = new TurbineAnalyzer( parent );
break;
case 2:
analyzer = new Sonogram( parent );
break;
case 3:
analyzer = new BoomAnalyzer( parent );
break;
#ifdef HAVE_QGLWIDGET
case 4:
analyzer = new GLAnalyzer( parent );
break;
case 5:
analyzer = new GLAnalyzer2( parent );
break;
case 6:
analyzer = new GLAnalyzer3( parent );
break;
case 7:
#else
case 4:
#endif
analyzer = new QLabel( i18n( "Click for Analyzers" ), parent ); //blank analyzer to satisfy Grue
static_cast<QLabel *>(analyzer)->setAlignment( Qt::AlignCenter );
break;
default:
AmarokConfig::setCurrentPlaylistAnalyzer( 0 );
case 0:
analyzer = new BlockAnalyzer( parent );
break;
}
return analyzer;
}

View File

@ -0,0 +1,167 @@
//
//
// C++ Implementation: $MODULE$
//
// Description:
//
//
// Author: Mark Kretschmann <markey@web.de>, (C) 2003
//
// Copyright: See COPYING file that comes with this distribution
//
//
#include "baranalyzer.h"
#include <cmath> //log10(), etc.
#include <QtDebug>
#include <QPainter>
BarAnalyzer::BarAnalyzer( QWidget *parent )
: Analyzer::Base( parent, 12, 8 )
//, m_bands( BAND_COUNT )
//, barVector( BAND_COUNT, 0 )
//, roofVector( BAND_COUNT, 50 )
//, roofVelocityVector( BAND_COUNT, ROOF_VELOCITY_REDUCTION_FACTOR )
{
//roof pixmaps don't depend on size() so we do in the ctor
m_bg = parent->palette().color(QPalette::Background);
QColor fg( 0xff, 0x50, 0x70 );
double dr = double(m_bg.red() - fg.red()) / (NUM_ROOFS-1); //-1 because we start loop below at 0
double dg = double(m_bg.green() - fg.green()) / (NUM_ROOFS-1);
double db = double(m_bg.blue() - fg.blue()) / (NUM_ROOFS-1);
for ( uint i = 0; i < NUM_ROOFS; ++i )
{
m_pixRoof[i] = QPixmap( COLUMN_WIDTH, 1 );
m_pixRoof[i].fill( QColor( fg.red()+int(dr*i), fg.green()+int(dg*i), fg.blue()+int(db*i) ) );
}
}
void BarAnalyzer::resizeEvent( QResizeEvent * e )
{
qDebug() << "Baranalyzer Resized(" << width() << "x" << height() << ")";
init();
}
// METHODS =====================================================
void BarAnalyzer::init()
{
const double MAX_AMPLITUDE = 1.0;
const double F = double(height() - 2) / (log10( 255 ) * MAX_AMPLITUDE );
BAND_COUNT = width() / 5;
MAX_DOWN = int(0 -(qMax(1, height() / 50)));
MAX_UP = int(qMax(1, height() / 25));
qDebug() << "BAND_COUNT = " << BAND_COUNT << " MAX_UP = " << MAX_UP << "MAX_DOWN = " << MAX_DOWN;
barVector.resize( BAND_COUNT, 0 );
roofVector.resize( BAND_COUNT, height() -5 );
roofVelocityVector.resize( BAND_COUNT, ROOF_VELOCITY_REDUCTION_FACTOR );
m_roofMem.resize(BAND_COUNT);
m_scope.resize(BAND_COUNT);
//generate a list of values that express amplitudes in range 0-MAX_AMP as ints from 0-height() on log scale
for ( uint x = 0; x < 256; ++x )
{
m_lvlMapper[x] = uint( F * log10( x+1 ) );
}
m_pixBarGradient = QPixmap( height()*COLUMN_WIDTH, height() );
m_pixCompose = QPixmap( size() );
QPainter p( &m_pixBarGradient );
for ( int x=0, r=0x40, g=0x30, b=0xff, r2=255-r;
x < height(); ++x )
{
for ( int y = x; y > 0; --y )
{
const double fraction = (double)y / height();
// p.setPen( QColor( r + (int)(r2 * fraction), g, b - (int)(255 * fraction) ) );
p.setPen( QColor( r + (int)(r2 * fraction), g, b ) );
p.drawLine( x*COLUMN_WIDTH, height() - y, (x+1)*COLUMN_WIDTH, height() - y );
}
}
setMinimumSize( QSize( BAND_COUNT * COLUMN_WIDTH, 10 ) );
}
void BarAnalyzer::analyze( QPainter& p, const Scope &s )
{
//Analyzer::interpolate( s, m_bands );
Scope &v = m_scope;
Analyzer::interpolate( s, v );
for ( uint i = 0, x = 0, y2; i < v.size(); ++i, x+=COLUMN_WIDTH+1 )
{
//assign pre[log10]'d value
y2 = uint(v[i] * 256); //256 will be optimised to a bitshift //no, it's a float
y2 = m_lvlMapper[ (y2 > 255) ? 255 : y2 ]; //lvlMapper is array of ints with values 0 to height()
int change = y2 - barVector[i];
//using the best of Markey's, piggz and Max's ideas on the way to shift the bars
//we have the following:
// 1. don't adjust shift when doing small up movements
// 2. shift large upwards with a bias towards last value
// 3. fall downwards at a constant pace
/*if ( change > MAX_UP ) //anything too much greater than 2 gives "jitter"
//add some dynamics - makes the value slightly closer to what it was last time
y2 = ( barVector[i] + MAX_UP );
//y2 = ( barVector[i] * 2 + y2 ) / 3;
else*/ if ( change < MAX_DOWN )
y2 = barVector[i] + MAX_DOWN;
if ( (int)y2 > roofVector[i] )
{
roofVector[i] = (int)y2;
roofVelocityVector[i] = 1;
}
//remember where we are
barVector[i] = y2;
if ( m_roofMem[i].size() > NUM_ROOFS )
m_roofMem[i].erase( m_roofMem[i].begin() );
//blt last n roofs, a.k.a motion blur
for ( uint c = 0; c < m_roofMem[i].size(); ++c )
//bitBlt( m_pComposePixmap, x, m_roofMem[i]->at( c ), m_roofPixmaps[ c ] );
//bitBlt( canvas(), x, m_roofMem[i][c], &m_pixRoof[ NUM_ROOFS - 1 - c ] );
p.drawPixmap(x, m_roofMem[i][c], m_pixRoof[ NUM_ROOFS - 1 - c ]);
//blt the bar
p.drawPixmap(x, height() - y2,
*gradient(), y2 * COLUMN_WIDTH, height() - y2, COLUMN_WIDTH, y2);
/*bitBlt( canvas(), x, height() - y2,
gradient(), y2 * COLUMN_WIDTH, height() - y2, COLUMN_WIDTH, y2, Qt::CopyROP );*/
m_roofMem[i].push_back( height() - roofVector[i] - 2 );
//set roof parameters for the NEXT draw
if ( roofVelocityVector[i] != 0 )
{
if ( roofVelocityVector[i] > 32 ) //no reason to do == 32
roofVector[i] -= (roofVelocityVector[i] - 32) / 20; //trivial calculation
if ( roofVector[i] < 0 )
{
roofVector[i] = 0; //not strictly necessary
roofVelocityVector[i] = 0;
}
else ++roofVelocityVector[i];
}
}
}

View File

@ -0,0 +1,57 @@
// Maintainer: Max Howell <max.howell@methylblue.com>
// Authors: Mark Kretschmann & Max Howell (C) 2003-4
// Copyright: See COPYING file that comes with this distribution
//
#ifndef BARANALYZER_H
#define BARANALYZER_H
#include "analyzerbase.h"
typedef std::vector<uint> aroofMemVec;
class BarAnalyzer : public Analyzer::Base
{
public:
BarAnalyzer( QWidget* );
void init();
virtual void analyze( QPainter& p, const Scope& );
//virtual void transform( Scope& );
/**
* Resizes the widget to a new geometry according to @p e
* @param e The resize-event
*/
void resizeEvent( QResizeEvent * e);
uint BAND_COUNT;
int MAX_DOWN;
int MAX_UP;
static const uint ROOF_HOLD_TIME = 48;
static const int ROOF_VELOCITY_REDUCTION_FACTOR = 32;
static const uint NUM_ROOFS = 16;
static const uint COLUMN_WIDTH = 4;
protected:
QPixmap m_pixRoof[NUM_ROOFS];
//vector<uint> m_roofMem[BAND_COUNT];
//Scope m_bands; //copy of the Scope to prevent creating/destroying a Scope every iteration
uint m_lvlMapper[256];
std::vector<aroofMemVec> m_roofMem;
std::vector<uint> barVector; //positions of bars
std::vector<int> roofVector; //positions of roofs
std::vector<uint> roofVelocityVector; //speed that roofs falls
const QPixmap *gradient() const { return &m_pixBarGradient; }
private:
QPixmap m_pixBarGradient;
QPixmap m_pixCompose;
Scope m_scope; //so we don't create a vector every frame
QColor m_bg;
};
#endif

View File

@ -0,0 +1,401 @@
// 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
//
#define DEBUG_PREFIX "BlockAnalyzer"
#include "blockanalyzer.h"
#include <cmath>
#include <QMouseEvent>
#include <QResizeEvent>
#include <QPainter>
#include <cstdlib>
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;
BlockAnalyzer::BlockAnalyzer( QWidget *parent )
: Analyzer::Base( parent, 20, 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());
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
const double fallTime = 30 * m_rows;
m_step = double(m_rows * timeout()) / fallTime;
}
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 )
{
// 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
Analyzer::interpolate( s, m_scope );
// Paint the background
p.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));
p.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
p.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 )
p.drawPixmap(x*(WIDTH+1), int(m_store[x])*(HEIGHT+1) + m_y, m_topBarPixmap );
}
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 );
}

View File

@ -0,0 +1,62 @@
// 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
{
public:
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;
protected:
virtual void transform( Scope& );
virtual void analyze( QPainter& p, const Scope& );
virtual void resizeEvent( QResizeEvent* );
virtual void paletteChange( const QPalette& );
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;
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

View File

@ -0,0 +1,157 @@
// Author: Max Howell <max.howell@methylblue.com>, (C) 2004
// Copyright: See COPYING file that comes with this distribution
#include "amarok.h"
#include "boomanalyzer.h"
#include <cmath>
#include <qlabel.h>
#include <qlayout.h>
#include <qpainter.h>
#include <qslider.h>
#include <qspinbox.h>
BoomAnalyzer::BoomAnalyzer( QWidget *parent )
: Analyzer::Base2D( parent, 10, 9 )
, K_barHeight( 1.271 )//1.471
, F_peakSpeed( 1.103 )//1.122
, F( 1.0 )
, bar_height( BAND_COUNT, 0 )
, peak_height( BAND_COUNT, 0 )
, peak_speed( BAND_COUNT, 0.01 )
, barPixmap( COLUMN_WIDTH, 50 )
{
QWidget *o, *box = new QWidget( this, 0, WType_TopLevel );
QSpinBox *m;
int v;
(new QGridLayout( box, 2, 3 ))->setAutoAdd( true );
v = int(K_barHeight*1000);
new QLabel( "Bar fall-rate:", box );
o = new QSlider( 100, 2000, 100, v, Qt::Horizontal, box );
(m = new QSpinBox( 100, 2000, 1, box ))->setValue( v );
connect( o, SIGNAL(valueChanged(int)), SLOT(changeK_barHeight( int )) );
connect( o, SIGNAL(valueChanged(int)), m, SLOT(setValue( int )) );
v = int(F_peakSpeed*1000);
new QLabel( "Peak acceleration: ", box );
o = new QSlider( 1000, 1300, 50, v, Qt::Horizontal, box );
(m = new QSpinBox( 1000, 1300, 1, box ))->setValue( v );
connect( o, SIGNAL(valueChanged(int)), SLOT(changeF_peakSpeed( int )) );
connect( o, SIGNAL(valueChanged(int)), m, SLOT(setValue( int )) );
//box->show();
}
void
BoomAnalyzer::changeK_barHeight( int newValue )
{
K_barHeight = (double)newValue / 1000;
}
void
BoomAnalyzer::changeF_peakSpeed( int newValue )
{
F_peakSpeed = (double)newValue / 1000;
}
void
BoomAnalyzer::init()
{
const uint HEIGHT = height() - 2;
const double h = 1.2 / HEIGHT;
F = double(HEIGHT) / (log10( 256 ) * 1.1 /*<- max. amplitude*/);
barPixmap.resize( COLUMN_WIDTH-2, HEIGHT );
QPainter p( &barPixmap );
for( uint y = 0; y < HEIGHT; ++y )
{
const double F = (double)y * h;
p.setPen( QColor( 255 - int(229.0 * F), 255 - int(229.0 * F), 255 - int(191.0 * F) ) );
p.drawLine( 0, y, COLUMN_WIDTH-2, y );
}
}
void
BoomAnalyzer::transform( Scope &s )
{
float *front = static_cast<float*>( &s.front() );
m_fht->spectrum( front );
m_fht->scale( front, 1.0 / 60 );
Scope scope( 32, 0 );
const uint xscale[] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,24,29,36,43,52,63,76,91,108,129,153,182,216,255 };
for( uint j, i = 0; i < 32; i++ )
for( j = xscale[i]; j < xscale[i + 1]; j++ )
if ( s[j] > scope[i] )
scope[i] = s[j];
s = scope;
}
void
BoomAnalyzer::analyze( const Scope &scope )
{
eraseCanvas();
QPainter p( canvas() );
float h;
const uint MAX_HEIGHT = height() - 1;
for( uint i = 0, x = 0, y; i < BAND_COUNT; ++i, x += COLUMN_WIDTH+1 )
{
h = log10( scope[i]*256.0 ) * F;
if( h > MAX_HEIGHT )
h = MAX_HEIGHT;
if( h > bar_height[i] )
{
bar_height[i] = h;
if( h > peak_height[i] )
{
peak_height[i] = h;
peak_speed[i] = 0.01;
}
else goto peak_handling;
}
else
{
if( bar_height[i] > 0.0 )
{
bar_height[i] -= K_barHeight; //1.4
if( bar_height[i] < 0.0 ) bar_height[i] = 0.0;
}
peak_handling:
if( peak_height[i] > 0.0 )
{
peak_height[i] -= peak_speed[i];
peak_speed[i] *= F_peakSpeed; //1.12
if( peak_height[i] < bar_height[i] ) peak_height[i] = bar_height[i];
if( peak_height[i] < 0.0 ) peak_height[i] = 0.0;
}
}
y = height() - uint(bar_height[i]);
bitBlt( canvas(), x+1, y, &barPixmap, 0, y );
p.setPen( Amarok::ColorScheme::Foreground );
p.drawRect( x, y, COLUMN_WIDTH, height() - y );
y = height() - uint(peak_height[i]);
p.setPen( Amarok::ColorScheme::Text );
p.drawLine( x, y, x+COLUMN_WIDTH-1, y );
}
}
#include "boomanalyzer.moc"

View File

@ -0,0 +1,52 @@
// Author: Max Howell <max.howell@methylblue.com>, (C) 2004
// Copyright: See COPYING file that comes with this distribution
//
#ifndef BOOMANALYZER_H
#define BOOMANALYZER_H
#include "analyzerbase.h"
/**
@author Max Howell
*/
class BoomAnalyzer : public Analyzer::Base2D
{
Q_OBJECT
public:
BoomAnalyzer( QWidget* );
virtual void init();
virtual void transform( Scope &s );
virtual void analyze( const Scope& );
public slots:
void changeK_barHeight( int );
void changeF_peakSpeed( int );
protected:
static const uint COLUMN_WIDTH = 4;
static const uint BAND_COUNT = 32;
double K_barHeight, F_peakSpeed, F;
std::vector<float> bar_height;
std::vector<float> peak_height;
std::vector<float> peak_speed;
QPixmap barPixmap;
};
namespace Amarok
{
namespace ColorScheme
{
extern QColor Base;
extern QColor Text;
extern QColor Background;
extern QColor Foreground;
}
}
#endif

View File

@ -0,0 +1,342 @@
/***************************************************************************
gloscope.cpp - description
-------------------
begin : Jan 17 2004
copyright : (C) 2004 by Adam Pigg
email : adam@piggz.co.uk
***************************************************************************/
/***************************************************************************
* *
* 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 <config.h>
#ifdef HAVE_QGLWIDGET
#include <cmath>
#include "glanalyzer.h"
#include <kdebug.h>
GLAnalyzer::GLAnalyzer( QWidget *parent )
: Analyzer::Base3D(parent, 15)
, m_oldy(32, -10.0f)
, m_peaks(32)
{}
GLAnalyzer::~GLAnalyzer()
{}
// METHODS =====================================================
void GLAnalyzer::analyze( const Scope &s )
{
//kdDebug() << "Scope Size: " << s.size() << endl;
/* Scope t(32);
if (s.size() != 32)
{
Analyzer::interpolate(s, t);
}
else
{
t = s;
}*/
uint offset = 0;
static float peak;
float mfactor = 0.0;
static int drawcount;
if (s.size() == 64)
{
offset=8;
}
glRotatef(0.25f, 0.0f, 1.0f, 0.5f); //Rotate the scene
drawFloor();
drawcount++;
if (drawcount > 25)
{
drawcount = 0;
peak = 0.0;
}
for ( uint i = 0; i < 32; i++ )
{
if (s[i] > peak)
{
peak = s[i];
}
}
mfactor = 20 / peak;
for ( uint i = 0; i < 32; i++ )
{
//kdDebug() << "Scope item " << i << " value: " << s[i] << endl;
// Calculate new horizontal position (x) depending on number of samples
x = -16.0f + i;
// Calculating new vertical position (y) depending on the data passed by amarok
y = float(s[i+offset] * mfactor); //This make it kinda dynamically resize depending on the data
//Some basic bounds checking
if (y > 30)
y = 30;
else if (y < 0)
y = 0;
if((y - m_oldy[i]) < -0.6f) // Going Down Too Much
{
y = m_oldy[i] - 0.7f;
}
if (y < 0.0f)
{
y = 0.0f;
}
m_oldy[i] = y; //Save value as last value
//Peak Code
if (m_oldy[i] > m_peaks[i].level)
{
m_peaks[i].level = m_oldy[i];
m_peaks[i].delay = 30;
}
if (m_peaks[i].delay > 0)
{
m_peaks[i].delay--;
}
if (m_peaks[i].level > 1.0f)
{
if (m_peaks[i].delay <= 0)
{
m_peaks[i].level-=0.4f;
}
}
// Draw the bar
drawBar(x,y);
drawPeak(x, m_peaks[i].level);
}
updateGL();
}
void GLAnalyzer::initializeGL()
{
// Clear frame (next fading will be preferred to clearing)
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);// Set clear color to black
glClear( GL_COLOR_BUFFER_BIT );
// Set the shading model
glShadeModel(GL_SMOOTH);
// Set the polygon mode to fill
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
// Enable depth testing for hidden line removal
glEnable(GL_DEPTH_TEST);
// Set blend parameters for 'composting alpha'
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
}
void GLAnalyzer::resizeGL( int w, int h )
{
glViewport( 0, 0, (GLint)w, (GLint)h );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho(-16.0f, 16.0f, -10.0f, 10.0f, -50.0f, 100.0f);
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
}
void GLAnalyzer::paintGL()
{
glMatrixMode( GL_MODELVIEW );
#if 0
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
#else
glEnable( GL_DEPTH_TEST );
glEnable( GL_BLEND );
glPushMatrix();
glLoadIdentity();
glBegin( GL_TRIANGLE_STRIP );
glColor4f( 0.0f, 0.0f, 0.1f, 0.08f );
glVertex2f( 20.0f, 10.0f );
glVertex2f( -20.0f, 10.0f );
glVertex2f( 20.0f, -10.0f );
glVertex2f( -20.0f, -10.0f );
glEnd();
glPopMatrix();
glDisable( GL_BLEND );
glEnable( GL_DEPTH_TEST );
glClear( GL_DEPTH_BUFFER_BIT );
#endif
//swapBuffers();
glFlush();
}
void GLAnalyzer::drawBar(float xPos, float height)
{
glPushMatrix();
//Sets color to blue
//Set the colour depending on the height of the bar
glColor3f((height/40) + 0.5f, (height/40) + 0.625f, 1.0f);
glTranslatef(xPos, -10.0f, 0.0f);
glScalef(1.0f, height, 3.0f);
drawCube();
//Set colour to full blue
//glColor3f(0.0f, 0.0f, 1.0f);
//drawFrame();
glPopMatrix();
}
void GLAnalyzer::drawFloor()
{
glPushMatrix();
//Sets color to amarok blue
glColor3f( 0.5f, 0.625f, 1.0f);
glTranslatef(-16.0f,-11.0f, -4.0f);
glScalef(32.0f, 1.0f, 10.0f);
drawCube();
//Set colour to full blue
glColor3f(0.0f, 0.0f, 1.0f);
drawFrame();
glPopMatrix();
}
void GLAnalyzer::drawPeak(float xPos, float ypos)
{
glPushMatrix();
//Set the colour to red
glColor3f(1.0f, 0.0f, 0.0f);
glTranslatef(xPos, ypos - 10.0f, 0.0f);
glScalef(1.0f, 1.0f, 3.0f);
drawCube();
glPopMatrix();
}
void GLAnalyzer::drawCube()
{
glPushMatrix();
glBegin(GL_POLYGON);
//This is the top face
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
//This is the front face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the right face
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
//This is the left face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the bottom face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the back face
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glEnd();
glPopMatrix();
}
void GLAnalyzer::drawFrame()
{
glPushMatrix();
glBegin(GL_LINES);
//This is the top face
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
//This is the front face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the right face
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
//This is the left face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 0.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the bottom face
glVertex3f(0.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 0.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 0.0f);
//This is the back face
glVertex3f(0.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 0.0f, 1.0f);
glVertex3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 1.0f, 1.0f);
glVertex3f(0.0f, 0.0f, 1.0f);
glEnd();
glPopMatrix();
}
#endif

View File

@ -0,0 +1,62 @@
/***************************************************************************
gloscope.h - description
-------------------
begin : Jan 17 2004
copyright : (C) 2004 by Adam Pigg
email : adam@piggz.co.uk
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
#ifndef GLOSCOPE_H
#define GLOSCOPE_H
#include <config.h>
#ifdef HAVE_QGLWIDGET
#include "analyzerbase.h"
/**
*@author piggz
*/
typedef struct
{
float level;
uint delay;
}
peak_tx;
class GLAnalyzer : public Analyzer::Base3D
{
private:
std::vector<float> m_oldy;
std::vector<peak_tx> m_peaks;
void drawCube();
void drawFrame();
void drawBar(float xPos, float height);
void drawPeak(float xPos, float ypos);
void drawFloor();
GLfloat x, y;
public:
GLAnalyzer(QWidget *);
~GLAnalyzer();
void analyze( const Scope & );
protected:
void initializeGL();
void resizeGL( int w, int h );
void paintGL();
};
#endif
#endif

View File

@ -0,0 +1,333 @@
/***************************************************************************
glanalyzer2.cpp - description
-------------------
begin : Feb 16 2004
copyright : (C) 2004 by Enrico Ros
email : eros.kde@email.it
***************************************************************************/
/***************************************************************************
* *
* 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 <config.h>
#ifdef HAVE_QGLWIDGET
#include <cmath>
#include <cstdlib>
#include "glanalyzer2.h"
#include <kdebug.h>
#include <kstandarddirs.h>
#include <qimage.h>
#include <sys/time.h>
GLAnalyzer2::GLAnalyzer2( QWidget *parent ):
Analyzer::Base3D(parent, 15)
{
//initialize openGL context before managing GL calls
makeCurrent();
loadTexture( locate("data","amarok/data/dot.png"), dotTexture );
loadTexture( locate("data","amarok/data/wirl1.png"), w1Texture );
loadTexture( locate("data","amarok/data/wirl2.png"), w2Texture );
show.paused = true;
show.pauseTimer = 0.0;
show.rotDegrees = 0.0;
frame.rotDegrees = 0.0;
}
GLAnalyzer2::~GLAnalyzer2()
{
freeTexture( dotTexture );
freeTexture( w1Texture );
freeTexture( w2Texture );
}
void GLAnalyzer2::initializeGL()
{
// Set a smooth shade model
glShadeModel(GL_SMOOTH);
// Disable depth test (all is drawn on a 2d plane)
glDisable(GL_DEPTH_TEST);
// Set blend parameters for 'composting alpha'
glBlendFunc( GL_SRC_ALPHA, GL_ONE ); //superpose
//glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); //fade
glEnable( GL_BLEND );
// Clear frame with a black background
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear( GL_COLOR_BUFFER_BIT );
}
void GLAnalyzer2::resizeGL( int w, int h )
{
// Setup screen. We're going to manually do the perspective projection
glViewport( 0, 0, (GLint)w, (GLint)h );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glOrtho( -10.0f, 10.0f, -10.0f, 10.0f, -5.0f, 5.0f );
// Get the aspect ratio of the screen to draw 'cicular' particles
float ratio = (float)w / (float)h,
eqPixH = 60,
eqPixW = 80;
if ( ratio >= (4.0/3.0) ) {
unitX = 10.0 / (eqPixH * ratio);
unitY = 10.0 / eqPixH;
} else {
unitX = 10.0 / eqPixW;
unitY = 10.0 / (eqPixW / ratio);
}
// Get current timestamp.
timeval tv;
gettimeofday( &tv, NULL );
show.timeStamp = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
}
void GLAnalyzer2::paused()
{
analyze( Scope() );
}
void GLAnalyzer2::analyze( const Scope &s )
{
bool haveNoData = s.empty();
// if we're going into pause mode, clear timers.
if ( !show.paused && haveNoData )
show.pauseTimer = 0.0;
// if we have got data, interpolate it (asking myself why I'm doing it here..)
if ( !(show.paused = haveNoData) )
{
int bands = s.size(),
lowbands = bands / 4,
hibands = bands / 3,
midbands = bands - lowbands - hibands; Q_UNUSED( midbands );
float currentEnergy = 0,
currentMeanBand = 0,
maxValue = 0;
for ( int i = 0; i < bands; i++ )
{
float value = s[i];
currentEnergy += value;
currentMeanBand += (float)i * value;
if ( value > maxValue )
maxValue = value;
}
frame.silence = currentEnergy < 0.001;
if ( !frame.silence )
{
frame.meanBand = 100.0 * currentMeanBand / (currentEnergy * bands);
currentEnergy = 100.0 * currentEnergy / (float)bands;
frame.dEnergy = currentEnergy - frame.energy;
frame.energy = currentEnergy;
// printf( "%d [%f :: %f ]\t%f \n", bands, frame.energy, frame.meanBand, maxValue );
} else
frame.energy = 0.0;
}
// update the frame
updateGL();
}
void GLAnalyzer2::paintGL()
{
// Compute the dT since the last call to paintGL and update timings
timeval tv;
gettimeofday( &tv, NULL );
double currentTime = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
show.dT = currentTime - show.timeStamp;
show.timeStamp = currentTime;
// Clear frame
glClear( GL_COLOR_BUFFER_BIT );
// Shitch to MODEL matrix and reset it to default
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
// Fade the previous drawings.
/* glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glBegin( GL_TRIANGLE_STRIP );
glColor4f( 0.0f, 0.0f, 0.0f, 0.2f );
glVertex2f( 10.0f, 10.0f );
glVertex2f( -10.0f, 10.0f );
glVertex2f( 10.0f, -10.0f );
glVertex2f( -10.0f, -10.0f );
glEnd();*/
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_TEXTURE_2D );
float alphaN = show.paused ? 0.2 : (frame.energy / 10.0),
alphaP = show.paused ? 1.0 : (1 - frame.energy / 20.0);
if ( alphaN > 1.0 )
alphaN = 1.0;
if ( alphaP < 0.1 )
alphaP = 0.1;
glBindTexture( GL_TEXTURE_2D, w2Texture );
setTextureMatrix( show.rotDegrees, 0.707*alphaP );
glColor4f( 1.0f, 1.0f, 1.0f, 1.0f );
glBegin( GL_TRIANGLE_STRIP );
glTexCoord2f( 1.0, 1.0 );
glVertex2f( 10.0f, 10.0f );
glTexCoord2f( 0.0, 1.0 );
glVertex2f( -10.0f, 10.0f );
glTexCoord2f( 1.0, 0.0 );
glVertex2f( 10.0f, -10.0f );
glTexCoord2f( 0.0 , 0.0 );
glVertex2f( -10.0f, -10.0f );
glEnd();
glBindTexture( GL_TEXTURE_2D, w1Texture );
setTextureMatrix( -show.rotDegrees * 2, 0.707 );
glColor4f( 1.0f, 1.0f, 1.0f, alphaN );
glBegin( GL_TRIANGLE_STRIP );
glTexCoord2f( 1.0, 1.0 );
glVertex2f( 10.0f, 10.0f );
glTexCoord2f( 0.0, 1.0 );
glVertex2f( -10.0f, 10.0f );
glTexCoord2f( 1.0, 0.0 );
glVertex2f( 10.0f, -10.0f );
glTexCoord2f( 0.0 , 0.0 );
glVertex2f( -10.0f, -10.0f );
glEnd();
setTextureMatrix( 0.0, 0.0 );
glDisable( GL_TEXTURE_2D );
glBlendFunc( GL_SRC_ALPHA, GL_ONE );
// Here begins the real draw loop
// some updates to the show
show.rotDegrees += 40.0 * show.dT;
frame.rotDegrees += 80.0 * show.dT;
// handle the 'pause' status
if ( show.paused )
{
if ( show.pauseTimer > 0.5 )
{
if ( show.pauseTimer > 0.6 )
show.pauseTimer -= 0.6;
drawFullDot( 0.0f, 0.4f, 0.8f, 1.0f );
drawFullDot( 0.0f, 0.4f, 0.8f, 1.0f );
}
show.pauseTimer += show.dT;
return;
}
if ( dotTexture ) {
glEnable( GL_TEXTURE_2D );
glBindTexture( GL_TEXTURE_2D, dotTexture );
} else
glDisable( GL_TEXTURE_2D );
glLoadIdentity();
// glRotatef( -frame.rotDegrees, 0,0,1 );
glBegin( GL_QUADS );
// Particle * particle = particleList.first();
// for (; particle; particle = particleList.next())
{
glColor4f( 0.0f, 1.0f, 0.0f, 1.0f );
drawDot( 0, 0, kMax(10.0,(10.0 * frame.energy)) );
glColor4f( 1.0f, 0.0f, 0.0f, 1.0f );
drawDot( 6, 0, kMax(10.0, (5.0 * frame.energy)) );
glColor4f( 0.0f, 0.4f, 1.0f, 1.0f );
drawDot( -6, 0, kMax(10.0, (5.0 * frame.energy)) );
}
glEnd();
}
void GLAnalyzer2::drawDot( float x, float y, float size )
{
float sizeX = size * unitX,
sizeY = size * unitY,
pLeft = x - sizeX,
pTop = y + sizeY,
pRight = x + sizeX,
pBottom = y - sizeY;
glTexCoord2f( 0, 0 ); // Bottom Left
glVertex2f( pLeft, pBottom );
glTexCoord2f( 0, 1 ); // Top Left
glVertex2f( pLeft, pTop );
glTexCoord2f( 1, 1 ); // Top Right
glVertex2f( pRight, pTop );
glTexCoord2f( 1, 0 ); // Bottom Right
glVertex2f( pRight, pBottom );
}
void GLAnalyzer2::drawFullDot( float r, float g, float b, float a )
{
glBindTexture( GL_TEXTURE_2D, dotTexture );
glEnable( GL_TEXTURE_2D );
glColor4f( r, g, b, a );
glBegin( GL_TRIANGLE_STRIP );
glTexCoord2f( 1.0, 1.0 );
glVertex2f( 10.0f, 10.0f );
glTexCoord2f( 0.0, 1.0 );
glVertex2f( -10.0f, 10.0f );
glTexCoord2f( 1.0, 0.0 );
glVertex2f( 10.0f, -10.0f );
glTexCoord2f( 0.0 , 0.0 );
glVertex2f( -10.0f, -10.0f );
glEnd();
glDisable( GL_TEXTURE_2D );
}
void GLAnalyzer2::setTextureMatrix( float rot, float scale )
{
glMatrixMode( GL_TEXTURE);
glLoadIdentity();
if ( rot != 0.0 || scale != 0.0 )
{
glTranslatef( 0.5f, 0.5f, 0.0f );
glRotatef( rot, 0.0f, 0.0f, 1.0f );
glScalef( scale, scale, 1.0f );
glTranslatef( -0.5f, -0.5f, 0.0f );
}
glMatrixMode( GL_MODELVIEW );
}
bool GLAnalyzer2::loadTexture( QString fileName, GLuint& textureID )
{
//reset texture ID to the default EMPTY value
textureID = 0;
//load image
QImage tmp;
if ( !tmp.load( fileName ) )
return false;
//convert it to suitable format (flipped RGBA)
QImage texture = QGLWidget::convertToGLFormat( tmp );
if ( texture.isNull() )
return false;
//get texture number and bind loaded image to that texture
glGenTextures( 1, &textureID );
glBindTexture( GL_TEXTURE_2D, textureID );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexImage2D( GL_TEXTURE_2D, 0, 4, texture.width(), texture.height(),
0, GL_RGBA, GL_UNSIGNED_BYTE, texture.bits() );
return true;
}
void GLAnalyzer2::freeTexture( GLuint & textureID )
{
if ( textureID > 0 )
glDeleteTextures( 1, &textureID );
textureID = 0;
}
#endif

View File

@ -0,0 +1,73 @@
/***************************************************************************
glanalyzer2.h - description
-------------------
begin : Feb 16 2004
copyright : (C) 2004 by Enrico Ros
email : eros.kde@email.it
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
#ifndef GLSTARVIEW_H
#define GLSTARVIEW_H
#include <config.h>
#ifdef HAVE_QGLWIDGET
#include "analyzerbase.h"
#include <qstring.h>
#include <qptrlist.h>
class GLAnalyzer2 : public Analyzer::Base3D
{
public:
GLAnalyzer2(QWidget *);
~GLAnalyzer2();
void analyze( const Scope & );
void paused();
protected:
void initializeGL();
void resizeGL( int w, int h );
void paintGL();
private:
struct ShowProperties {
bool paused;
double timeStamp;
double dT;
double pauseTimer;
float rotDegrees;
} show;
struct FrameProperties {
float energy;
float dEnergy;
float meanBand;
float rotDegrees;
bool silence;
} frame;
GLuint dotTexture;
GLuint w1Texture;
GLuint w2Texture;
float unitX, unitY;
void drawDot( float x, float y, float size );
void drawFullDot( float r, float g, float b, float a );
void setTextureMatrix( float rot, float scale );
bool loadTexture(QString file, GLuint& textureID);
void freeTexture(GLuint& textureID);
};
#endif
#endif

View File

@ -0,0 +1,480 @@
/***************************************************************************
glanalyzer3.cpp - Bouncing Ballzz
-------------------
begin : Feb 19 2004
copyright : (C) 2004 by Enrico Ros
email : eros.kde@email.it
***************************************************************************/
/***************************************************************************
* *
* 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 <config.h>
#ifdef HAVE_QGLWIDGET
#include <cmath>
#include <cstdlib>
#include "glanalyzer3.h"
#include <kdebug.h>
#include <kstandarddirs.h>
#include <qimage.h>
#include <sys/time.h>
#ifndef HAVE_FABSF
inline float fabsf(float f)
{
return f < 0.f ? -f : f;
}
#endif
class Ball
{
public:
Ball() : x( drand48() - drand48() ), y( 1 - 2.0 * drand48() ),
z( drand48() ), vx( 0.0 ), vy( 0.0 ), vz( 0.0 ),
mass( 0.01 + drand48()/10.0 )
//,color( (float[3]) { 0.0, drand48()*0.5, 0.7 + drand48() * 0.3 } )
{
//this is because GCC < 3.3 can't compile the above line, we aren't sure why though
color[0] = 0.0; color[1] = drand48()*0.5; color[2] = 0.7 + drand48() * 0.3;
};
float x, y, z, vx, vy, vz, mass;
float color[3];
void updatePhysics( float dT )
{
x += vx * dT; // position
y += vy * dT; // position
z += vz * dT; // position
if ( y < -0.8 ) vy = fabsf( vy );
if ( y > 0.8 ) vy = -fabsf( vy );
if ( z < 0.1 ) vz = fabsf( vz );
if ( z > 0.9 ) vz = -fabsf( vz );
vx += (( x > 0 ) ? 4.94 : -4.94) * dT; // G-force
vx *= (1 - 2.9 * dT); // air friction
vy *= (1 - 2.9 * dT); // air friction
vz *= (1 - 2.9 * dT); // air friction
}
};
class Paddle
{
public:
Paddle( float xPos ) : onLeft( xPos < 0 ), mass( 1.0 ),
X( xPos ), x( xPos ), vx( 0.0 ) {};
void updatePhysics( float dT )
{
x += vx * dT; // posision
vx += (1300 * (X - x) / mass) * dT; // elasticity
vx *= (1 - 4.0 * dT); // air friction
}
void renderGL()
{
glBegin( GL_TRIANGLE_STRIP );
glColor3f( 0.0f, 0.1f, 0.3f );
glVertex3f( x, -1.0f, 0.0 );
glVertex3f( x, 1.0f, 0.0 );
glColor3f( 0.1f, 0.2f, 0.6f );
glVertex3f( x, -1.0f, 1.0 );
glVertex3f( x, 1.0f, 1.0 );
glEnd();
}
void bounce( Ball * ball )
{
if ( onLeft && ball->x < x )
{
ball->vx = vx * mass / (mass + ball->mass) + fabsf( ball->vx );
ball->vy = (drand48() - drand48()) * 1.8;
ball->vz = (drand48() - drand48()) * 0.9;
ball->x = x;
}
else if ( !onLeft && ball->x > x )
{
ball->vx = vx * mass / (mass + ball->mass) - fabsf( ball->vx );
ball->vy = (drand48() - drand48()) * 1.8;
ball->vz = (drand48() - drand48()) * 0.9;
ball->x = x;
}
}
void impulse( float strength )
{
if ( (onLeft && strength > vx) || (!onLeft && strength < vx) )
vx += strength;
}
private:
bool onLeft;
float mass, X, x, vx;
};
GLAnalyzer3::GLAnalyzer3( QWidget *parent ):
Analyzer::Base3D(parent, 15)
{
//initialize openGL context before managing GL calls
makeCurrent();
loadTexture( locate("data","amarok/data/ball.png"), ballTexture );
loadTexture( locate("data","amarok/data/grid.png"), gridTexture );
balls.setAutoDelete( true );
leftPaddle = new Paddle( -1.0 );
rightPaddle = new Paddle( 1.0 );
for ( int i = 0; i < NUMBER_OF_BALLS; i++ )
balls.append( new Ball() );
show.colorK = 0.0;
show.gridScrollK = 0.0;
show.gridEnergyK = 0.0;
show.camRot = 0.0;
show.camRoll = 0.0;
show.peakEnergy = 1.0;
frame.silence = true;
frame.energy = 0.0;
frame.dEnergy = 0.0;
}
GLAnalyzer3::~GLAnalyzer3()
{
freeTexture( ballTexture );
freeTexture( gridTexture );
delete leftPaddle;
delete rightPaddle;
balls.clear();
}
void GLAnalyzer3::initializeGL()
{
// Set a smooth shade model
glShadeModel(GL_SMOOTH);
// Disable depth test (all is drawn 'z-sorted')
glDisable( GL_DEPTH_TEST );
// Set blending function (Alpha addition)
glBlendFunc( GL_SRC_ALPHA, GL_ONE );
// Clear frame with a black background
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
void GLAnalyzer3::resizeGL( int w, int h )
{
// Setup screen. We're going to manually do the perspective projection
glViewport( 0, 0, (GLint)w, (GLint)h );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
glFrustum( -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 4.5f );
// Get the aspect ratio of the screen to draw 'circular' particles
float ratio = (float)w / (float)h;
if ( ratio >= 1.0 ) {
unitX = 0.34 / ratio;
unitY = 0.34;
} else {
unitX = 0.34;
unitY = 0.34 * ratio;
}
// Get current timestamp.
timeval tv;
gettimeofday( &tv, NULL );
show.timeStamp = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
}
void GLAnalyzer3::paused()
{
analyze( Scope() );
}
void GLAnalyzer3::analyze( const Scope &s )
{
// compute the dTime since the last call
timeval tv;
gettimeofday( &tv, NULL );
double currentTime = (double)tv.tv_sec + (double)tv.tv_usec/1000000.0;
show.dT = currentTime - show.timeStamp;
show.timeStamp = currentTime;
// compute energy integrating frame's spectrum
if ( !s.empty() )
{
int bands = s.size();
float currentEnergy = 0,
maxValue = 0;
// integrate spectrum -> energy
for ( int i = 0; i < bands; i++ )
{
float value = s[i];
currentEnergy += value;
if ( value > maxValue )
maxValue = value;
}
currentEnergy *= 100.0 / (float)bands;
// emulate a peak detector: currentEnergy -> peakEnergy (3tau = 30 seconds)
show.peakEnergy = 1.0 + ( show.peakEnergy - 1.0 ) * exp( - show.dT / 10.0 );
if ( currentEnergy > show.peakEnergy )
show.peakEnergy = currentEnergy;
// check for silence
frame.silence = currentEnergy < 0.001;
// normalize frame energy against peak energy and compute frame stats
currentEnergy /= show.peakEnergy;
frame.dEnergy = currentEnergy - frame.energy;
frame.energy = currentEnergy;
} else
frame.silence = true;
// update the frame
updateGL();
}
void GLAnalyzer3::paintGL()
{
// limit max dT to 0.05 and update color and scroll constants
if ( show.dT > 0.05 )
show.dT = 0.05;
show.colorK += show.dT * 0.4;
if ( show.colorK > 3.0 )
show.colorK -= 3.0;
show.gridScrollK += 0.2 * show.peakEnergy * show.dT;
// Switch to MODEL matrix and clear screen
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glClear( GL_COLOR_BUFFER_BIT );
// Draw scrolling grid
if ( (show.gridEnergyK > 0.05) || (!frame.silence && frame.dEnergy < -0.3) )
{
show.gridEnergyK *= exp( -show.dT / 0.1 );
if ( -frame.dEnergy > show.gridEnergyK )
show.gridEnergyK = -frame.dEnergy*2.0;
float gridColor[4] = { 0.0, 1.0, 0.6, show.gridEnergyK };
drawScrollGrid( show.gridScrollK, gridColor );
}
// Roll camera up/down handling the beat
show.camRot += show.camRoll * show.dT; // posision
show.camRoll -= 400 * show.camRot * show.dT; // elasticity
show.camRoll *= (1 - 2.0 * show.dT); // friction
if ( !frame.silence && frame.dEnergy > 0.4 )
show.camRoll += show.peakEnergy*2.0;
glRotatef( show.camRoll / 2.0, 1,0,0 );
// Translate the drawing plane
glTranslatef( 0.0f, 0.0f, -1.8f );
// Draw upper/lower planes and paddles
drawHFace( -1.0 );
drawHFace( 1.0 );
leftPaddle->renderGL();
rightPaddle->renderGL();
// Draw Balls
if ( ballTexture ) {
glEnable( GL_TEXTURE_2D );
glBindTexture( GL_TEXTURE_2D, ballTexture );
} else
glDisable( GL_TEXTURE_2D );
glEnable( GL_BLEND );
Ball * ball = balls.first();
for ( ; ball; ball = balls.next() )
{
float color[3],
angle = show.colorK;
// Rotate the color based on 'angle' value [0,3)
if ( angle < 1.0 )
{
color[ 0 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
color[ 1 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
color[ 2 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
}
else if ( angle < 2.0 )
{
angle -= 1.0;
color[ 0 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
color[ 1 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
color[ 2 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
}
else
{
angle -= 2.0;
color[ 0 ] = ball->color[ 2 ] * (1 - angle) + ball->color[ 0 ] * angle;
color[ 1 ] = ball->color[ 0 ] * (1 - angle) + ball->color[ 1 ] * angle;
color[ 2 ] = ball->color[ 1 ] * (1 - angle) + ball->color[ 2 ] * angle;
}
// Draw the dot and update its physics also checking at bounces
glColor3fv( color );
drawDot3s( ball->x, ball->y, ball->z, 1.0 );
ball->updatePhysics( show.dT );
if ( ball->x < 0 )
leftPaddle->bounce( ball );
else
rightPaddle->bounce( ball );
}
glDisable( GL_BLEND );
glDisable( GL_TEXTURE_2D );
// Update physics of paddles
leftPaddle->updatePhysics( show.dT );
rightPaddle->updatePhysics( show.dT );
if ( !frame.silence )
{
leftPaddle->impulse( frame.energy*3.0 + frame.dEnergy*6.0 );
rightPaddle->impulse( -frame.energy*3.0 - frame.dEnergy*6.0 );
}
}
void GLAnalyzer3::drawDot3s( float x, float y, float z, float size )
{
// Circular XY dot drawing functions
float sizeX = size * unitX,
sizeY = size * unitY,
pXm = x - sizeX,
pXM = x + sizeX,
pYm = y - sizeY,
pYM = y + sizeY;
// Draw the Dot
glBegin( GL_QUADS );
glTexCoord2f( 0, 0 ); // Bottom Left
glVertex3f( pXm, pYm, z );
glTexCoord2f( 0, 1 ); // Top Left
glVertex3f( pXm, pYM, z );
glTexCoord2f( 1, 1 ); // Top Right
glVertex3f( pXM, pYM, z );
glTexCoord2f( 1, 0 ); // Bottom Right
glVertex3f( pXM, pYm, z );
glEnd();
// Shadow XZ drawing functions
float sizeZ = size / 10.0,
pZm = z - sizeZ,
pZM = z + sizeZ,
currentColor[4];
glGetFloatv( GL_CURRENT_COLOR, currentColor );
float alpha = currentColor[3],
topSide = (y + 1) / 4,
bottomSide = (1 - y) / 4;
// Draw the top shadow
currentColor[3] = topSide * topSide * alpha;
glColor4fv( currentColor );
glBegin( GL_QUADS );
glTexCoord2f( 0, 0 ); // Bottom Left
glVertex3f( pXm, 1, pZm );
glTexCoord2f( 0, 1 ); // Top Left
glVertex3f( pXm, 1, pZM );
glTexCoord2f( 1, 1 ); // Top Right
glVertex3f( pXM, 1, pZM );
glTexCoord2f( 1, 0 ); // Bottom Right
glVertex3f( pXM, 1, pZm );
glEnd();
// Draw the bottom shadow
currentColor[3] = bottomSide * bottomSide * alpha;
glColor4fv( currentColor );
glBegin( GL_QUADS );
glTexCoord2f( 0, 0 ); // Bottom Left
glVertex3f( pXm, -1, pZm );
glTexCoord2f( 0, 1 ); // Top Left
glVertex3f( pXm, -1, pZM );
glTexCoord2f( 1, 1 ); // Top Right
glVertex3f( pXM, -1, pZM );
glTexCoord2f( 1, 0 ); // Bottom Right
glVertex3f( pXM, -1, pZm );
glEnd();
}
void GLAnalyzer3::drawHFace( float y )
{
glBegin( GL_TRIANGLE_STRIP );
glColor3f( 0.0f, 0.1f, 0.2f );
glVertex3f( -1.0f, y, 0.0 );
glVertex3f( 1.0f, y, 0.0 );
glColor3f( 0.1f, 0.6f, 0.5f );
glVertex3f( -1.0f, y, 2.0 );
glVertex3f( 1.0f, y, 2.0 );
glEnd();
}
void GLAnalyzer3::drawScrollGrid( float scroll, float color[4] )
{
if ( !gridTexture )
return;
glMatrixMode( GL_TEXTURE );
glLoadIdentity();
glTranslatef( 0.0, -scroll, 0.0 );
glMatrixMode( GL_MODELVIEW );
float backColor[4] = { 1.0, 1.0, 1.0, 0.0 };
for ( int i = 0; i < 3; i++ )
backColor[ i ] = color[ i ];
glEnable( GL_TEXTURE_2D );
glBindTexture( GL_TEXTURE_2D, gridTexture );
glEnable( GL_BLEND );
glBegin( GL_TRIANGLE_STRIP );
glColor4fv( color ); // top face
glTexCoord2f( 0.0f, 1.0f );
glVertex3f( -1.0f, 1.0f, -1.0f );
glTexCoord2f( 1.0f, 1.0f );
glVertex3f( 1.0f, 1.0f, -1.0f );
glColor4fv( backColor ); // central points
glTexCoord2f( 0.0f, 0.0f );
glVertex3f( -1.0f, 0.0f, -3.0f );
glTexCoord2f( 1.0f, 0.0f );
glVertex3f( 1.0f, 0.0f, -3.0f );
glColor4fv( color ); // bottom face
glTexCoord2f( 0.0f, 1.0f );
glVertex3f( -1.0f, -1.0f, -1.0f );
glTexCoord2f( 1.0f, 1.0f );
glVertex3f( 1.0f, -1.0f, -1.0f );
glEnd();
glDisable( GL_BLEND );
glDisable( GL_TEXTURE_2D );
glMatrixMode( GL_TEXTURE );
glLoadIdentity();
glMatrixMode( GL_MODELVIEW );
}
bool GLAnalyzer3::loadTexture( QString fileName, GLuint& textureID )
{
//reset texture ID to the default EMPTY value
textureID = 0;
//load image
QImage tmp;
if ( !tmp.load( fileName ) )
return false;
//convert it to suitable format (flipped RGBA)
QImage texture = QGLWidget::convertToGLFormat( tmp );
if ( texture.isNull() )
return false;
//get texture number and bind loaded image to that texture
glGenTextures( 1, &textureID );
glBindTexture( GL_TEXTURE_2D, textureID );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexImage2D( GL_TEXTURE_2D, 0, 4, texture.width(), texture.height(),
0, GL_RGBA, GL_UNSIGNED_BYTE, texture.bits() );
return true;
}
void GLAnalyzer3::freeTexture( GLuint& textureID )
{
if ( textureID > 0 )
glDeleteTextures( 1, &textureID );
textureID = 0;
}
#endif

View File

@ -0,0 +1,80 @@
/***************************************************************************
glanalyzer3.h - description
-------------------
begin : Feb 16 2004
copyright : (C) 2004 by Enrico Ros
email : eros.kde@email.it
***************************************************************************/
/***************************************************************************
* *
* 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 <config.h>
#ifdef HAVE_QGLWIDGET
#ifndef GLBOUNCER_H
#define GLBOUNCER_H
#include "analyzerbase.h"
#include <qstring.h>
#include <qptrlist.h>
class QWidget;
class Ball;
class Paddle;
class GLAnalyzer3 : public Analyzer::Base3D
{
public:
GLAnalyzer3(QWidget *);
~GLAnalyzer3();
void analyze( const Scope & );
void paused();
protected:
void initializeGL();
void resizeGL( int w, int h );
void paintGL();
private:
struct ShowProperties {
double timeStamp;
double dT;
float colorK;
float gridScrollK;
float gridEnergyK;
float camRot;
float camRoll;
float peakEnergy;
} show;
struct FrameProperties {
bool silence;
float energy;
float dEnergy;
} frame;
static const int NUMBER_OF_BALLS = 16;
QPtrList<Ball> balls;
Paddle * leftPaddle, * rightPaddle;
float unitX, unitY;
GLuint ballTexture;
GLuint gridTexture;
void drawDot3s( float x, float y, float z, float size );
void drawHFace( float y );
void drawScrollGrid( float scroll, float color[4] );
bool loadTexture(QString file, GLuint& textureID);
void freeTexture(GLuint& textureID);
};
#endif
#endif

View File

@ -0,0 +1,91 @@
//
//
// C++ Implementation: Sonogram
//
// Description:
//
//
// Author: Melchior FRANZ <mfranz@kde.org>, (C) 2004
//
// Copyright: See COPYING file that comes with this distribution
//
//
#include <qpainter.h>
#include "sonogram.h"
Sonogram::Sonogram(QWidget *parent) :
Analyzer::Base2D(parent, 16, 9)
{
}
Sonogram::~Sonogram()
{
}
void Sonogram::init()
{
eraseCanvas();
}
void Sonogram::resizeEvent(QResizeEvent *e)
{
QWidget::resizeEvent(e);
canvas()->resize(size());
background()->resize(size());
//only for gcc < 4.0
#if !( __GNUC__ > 4 || ( __GNUC__ == 4 && __GNUC_MINOR__ >= 0 ) )
resizeForBands(height() < 128 ? 128 : height());
#endif
background()->fill(backgroundColor());
bitBlt(canvas(), 0, 0, background());
bitBlt(this, 0, 0, background());
}
void Sonogram::analyze(const Scope &s)
{
int x = width() - 1;
QColor c;
QPainter p(canvas());
bitBlt(canvas(), 0, 0, canvas(), 1, 0, x, height());
Scope::const_iterator it = s.begin(), end = s.end();
for (int y = height() - 1; y;) {
if (it >= end || *it < .005)
c = backgroundColor();
else if (*it < .05)
c.setHsv(95, 255, 255 - int(*it * 4000.0));
else if (*it < 1.0)
c.setHsv(95 - int(*it * 90.0), 255, 255);
else
c = Qt::red;
p.setPen(c);
p.drawPoint(x, y--);
if (it < end)
++it;
}
}
void Sonogram::transform(Scope &scope)
{
float *front = static_cast<float*>(&scope.front());
m_fht->power2(front);
m_fht->scale(front, 1.0 / 256);
scope.resize( m_fht->size() / 2 );
}
void Sonogram::demo()
{
analyze(Scope(m_fht->size(), 0));
}

37
src/analyzers/sonogram.h Normal file
View File

@ -0,0 +1,37 @@
//
//
// C++ Interface: Sonogram
//
// Description:
//
//
// Author: Melchior FRANZ <mfranz@kde.org>, (C) 2004
//
// Copyright: See COPYING file that comes with this distribution
//
//
#ifndef SONOGRAM_H
#define SONOGRAM_H
#include "analyzerbase.h"
/**
@author Melchior FRANZ
*/
class Sonogram : public Analyzer::Base2D
{
public:
Sonogram(QWidget*);
~Sonogram();
protected:
void init();
void analyze(const Scope&);
void transform(Scope&);
void demo();
void resizeEvent(QResizeEvent*);
};
#endif

78
src/analyzers/turbine.cpp Normal file
View File

@ -0,0 +1,78 @@
//
// Amarok BarAnalyzer 3 - Jet Turbine: Symmetric version of analyzer 1
//
// Author: Stanislav Karchebny <berkus@users.sf.net>, (C) 2003
// Max Howell (I modified it to use boom analyzer code)
//
// Copyright: like rest of Amarok
//
#include <cmath>
#include <qpainter.h>
#include "amarok.h"
#include "turbine.h"
void TurbineAnalyzer::analyze( const Scope &scope )
{
eraseCanvas();
QPainter p( canvas() );
float h;
const uint hd2 = height() / 2;
const uint MAX_HEIGHT = hd2 - 1;
for( uint i = 0, x = 0, y; i < BAND_COUNT; ++i, x += COLUMN_WIDTH+1 )
{
h = log10( scope[i]*256.0 ) * F * 0.5;
if( h > MAX_HEIGHT )
h = MAX_HEIGHT;
if( h > bar_height[i] )
{
bar_height[i] = h;
if( h > peak_height[i] )
{
peak_height[i] = h;
peak_speed[i] = 0.01;
}
else goto peak_handling;
}
else
{
if( bar_height[i] > 0.0 )
{
bar_height[i] -= K_barHeight; //1.4
if( bar_height[i] < 0.0 ) bar_height[i] = 0.0;
}
peak_handling:
if( peak_height[i] > 0.0 )
{
peak_height[i] -= peak_speed[i];
peak_speed[i] *= F_peakSpeed; //1.12
if( peak_height[i] < bar_height[i] ) peak_height[i] = bar_height[i];
if( peak_height[i] < 0.0 ) peak_height[i] = 0.0;
}
}
y = hd2 - uint(bar_height[i]);
bitBlt( canvas(), x+1, y, &barPixmap, 0, y );
bitBlt( canvas(), x+1, hd2, &barPixmap, 0, (int)bar_height[i] );
p.setPen( Amarok::ColorScheme::Foreground );
p.drawRect( x, y, COLUMN_WIDTH, (int)bar_height[i]*2 );
const uint x2 = x+COLUMN_WIDTH-1;
p.setPen( Amarok::ColorScheme::Text );
y = hd2 - uint(peak_height[i]);
p.drawLine( x, y, x2, y );
y = hd2 + uint(peak_height[i]);
p.drawLine( x, y, x2, y );
}
}

22
src/analyzers/turbine.h Normal file
View File

@ -0,0 +1,22 @@
//
// Amarok BarAnalyzer 3 - Jet Turbine: Symmetric version of analyzer 1
//
// Author: Stanislav Karchebny <berkus@users.sf.net>, (C) 2003
//
// Copyright: like rest of Amarok
//
#ifndef ANALYZER_TURBINE_H
#define ANALYZER_TURBINE_H
#include "boomanalyzer.h"
class TurbineAnalyzer : public BoomAnalyzer
{
public:
TurbineAnalyzer( QWidget *parent ) : BoomAnalyzer( parent ) {}
void analyze( const Scope& );
};
#endif

1
src/backgroundthread.cpp Normal file
View File

@ -0,0 +1 @@
#include "backgroundthread.h"

59
src/backgroundthread.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef BACKGROUNDTHREAD_H
#define BACKGROUNDTHREAD_H
#include <QThread>
#include <QtDebug>
#include <boost/shared_ptr.hpp>
class BackgroundThreadBase : public QThread {
Q_OBJECT
public:
BackgroundThreadBase(QObject* parent = 0) : QThread(parent) {}
signals:
void Initialised();
};
template <typename T>
class BackgroundThread : public BackgroundThreadBase {
public:
BackgroundThread(QObject* parent = 0);
~BackgroundThread();
boost::shared_ptr<T> Worker() const { return worker_; }
protected:
void run();
private:
boost::shared_ptr<T> worker_;
};
template <typename T>
BackgroundThread<T>::BackgroundThread(QObject *parent)
: BackgroundThreadBase(parent)
{
}
template <typename T>
BackgroundThread<T>::~BackgroundThread() {
if (isRunning()) {
quit();
if (wait(10000))
return;
terminate();
wait(10000);
}
}
template <typename T>
void BackgroundThread<T>::run() {
worker_.reset(new T);
emit Initialised();
exec();
worker_.reset();
}
#endif // BACKGROUNDTHREAD_H

16
src/directory.h Normal file
View File

@ -0,0 +1,16 @@
#ifndef DIRECTORY_H
#define DIRECTORY_H
#include <QList>
#include <QString>
class QSqlQuery;
struct Directory {
QString path;
int id;
};
typedef QList<Directory> DirectoryList;
#endif // DIRECTORY_H

26
src/engine_fwd.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef ENGINE_FWD_H
#define ENGINE_FWD_H
/// Used by eg engineobserver.h, and thus we reduce header dependencies on enginebase.h
namespace Engine
{
class SimpleMetaBundle;
class Base;
/**
* You should return:
* Playing when playing,
* Paused when paused
* Idle when you still have a URL loaded (ie you have not been told to stop())
* Empty when you have been told to stop(), or an error occurred and you stopped yourself
*
* It is vital to be Idle just after the track has ended!
*/
enum State { Empty, Idle, Playing, Paused };
}
typedef Engine::Base EngineBase;
#endif

49
src/enginebase.cpp Normal file
View File

@ -0,0 +1,49 @@
//Copyright: (C) 2003 Mark Kretschmann
// (C) 2004,2005 Max Howell, <max.howell@methylblue.com>
//License: See COPYING
#include "enginebase.h"
#include <cmath>
Engine::Base::Base()
: m_xfadeLength( 0 )
, m_xfadeNextTrack( false )
, m_volume( 50 )
, m_scope( SCOPESIZE )
, m_isStream( false )
{}
Engine::Base::~Base()
{
}
//////////////////////////////////////////////////////////////////////
bool
Engine::Base::load( const QUrl &url, bool stream )
{
m_url = url;
m_isStream = stream;
return true;
}
void Engine::Base::setVolume( uint value )
{
m_volume = value;
setVolumeSW( makeVolumeLogarithmic( value ) );
}
uint
Engine::Base::makeVolumeLogarithmic( uint volume ) // static
{
// We're using a logarithmic function to make the volume ramp more natural.
return static_cast<uint>( 100 - 100.0 * std::log10( ( 100 - volume ) * 0.09 + 1.0 ) );
}

281
src/enginebase.h Normal file
View File

@ -0,0 +1,281 @@
//Copyright: (C) 2003 Mark Kretschmann
// (C) 2004 Max Howell, <max.howell@methylblue.com>
//License: See COPYING
#ifndef AMAROK_ENGINEBASE_H
#define AMAROK_ENGINEBASE_H
#include <QUrl>
#include <QObject>
#include <QList>
#include <sys/types.h>
#include <vector>
/**
* @class Engine::Base
* @author Mark Kretshmann
* @author Max Howell
*
* This is an abstract base class that you need to derive when making your own backends.
* It is typdefed to EngineBase for your conveniece.
*
* The only key thing to get right is what to return from state(), as some Amarok
* behaviour is dependent on you returning the right state at the right time.
*
* Empty = No URL loaded and ready to play
* Idle = URL ready for play, but not playing, so before AND after playback
* Playing = Playing a stream
* Paused = Stream playback is paused
*
* Not returning idle when you have reached End-Of-Stream but Amarok has not told you
* to stop would be bad because some components behave differently when the engine is
* Empty or not. You are Idle because you still have a URL assigned.
*
* load( KURL ) is a key function because after this point your engine is loaded, and
* Amarok will expect you to be able to play the URL until stop() or another load() is
* called.
*
* You must handle your own media, do not rely on Amarok to call stop() before play() etc.
*
* At this time, emitting stateChanged( Engine::Idle ) is not necessary, otherwise you should
* let Amarok know of state changes so it updates the UI correctly.
*
* Basically, reimplement everything virtual and ensure you emit stateChanged() correctly,
* try not to block in any function that is called by Amarok, try to keep the user informed
* with emit statusText()
*
* Only canDecode() needs to be thread-safe. Everything else is only called from the GUI thread.
*/
#include "engine_fwd.h"
namespace Engine
{
typedef std::vector<int16_t> Scope;
class Base : public QObject
{
Q_OBJECT
signals:
/** Emitted when end of current track is reached. */
void trackEnded();
/** Transmits status message, the message disappears after ~2s. */
void statusText( const QString& );
/**
* Shows a long message in a non-invasive manner, you should prefer
* this over KMessageBoxes, but do use KMessageBox when you must
* interrupt the user or the message is very important.
*/
void infoMessage( const QString& );
/** Transmits metadata package. */
void metaData( const Engine::SimpleMetaBundle& );
/** Signals that a SYNC has been recieved, and new last.fm data needs to be downloaded */
void lastFmTrackChange();
/** Signals a change in the engine's state. */
void stateChanged( Engine::State );
/** Shows Amarok config dialog at specified page */
void showConfigDialog( const QString& );
public:
virtual ~Base();
/**
* Initializes the engine. Must be called after the engine was loaded.
* @return True if initialization was successful.
*/
virtual bool init() = 0;
/**
* Determines if the engine is able to play a given URL.
* @param url The URL of the file/stream.
* @return True if we can play the URL.
*/
virtual bool canDecode( const QUrl &url ) const = 0;
/**
* Determines if current track is a stream.
* @return True if track is a stream.
*/
inline bool isStream() { return m_isStream; }
/**
* Load new track for playing.
* @param url URL to be played.
* @param stream True if URL is a stream.
* @return True for success.
*/
virtual bool load( const QUrl &url, bool stream = false );
/**
* Load new track and start Playback. Convenience function for Amarok to use.
* @param url URL to be played.
* @param stream True if URL is a stream.
* @return True for success.
*/
bool play( const QUrl &u, bool stream = false ) { return load( u, stream ) && play(); }
/**
* Start playback.
* @param offset Start playing at @p msec position.
* @return True for success.
*/
virtual bool play( uint offset = 0 ) = 0;
/** Stops playback */
virtual void stop() = 0;
/** Pauses playback */
virtual void pause() = 0;
/** Resumes playback if paused */
virtual void unpause() = 0;
/**
* Get current engine status.
* @return the correct State as described at the enum
*/
virtual State state() const = 0;
/** Get time position (msec). */
virtual uint position() const = 0;
/** Get track length (msec). */
virtual uint length() const { return 0; }
/**
* Jump to new time position.
* @param ms New position.
*/
virtual void seek( uint ms ) = 0;
/**
* Determines whether media is currently loaded.
* @return True if media is loaded, system is ready to play.
*/
inline bool loaded() const { return state() != Empty; }
inline uint volume() const { return m_volume; }
/**
* Fetch the current audio sample buffer.
* @return Audio sample buffer.
*/
virtual const Scope &scope() { return m_scope; };
/**
* Set new volume value.
* @param value Volume in range 0 to 100.
*/
void setVolume( uint value );
/** Set new crossfade length (msec) */
void setXfadeLength( int value ) { m_xfadeLength = value; }
/** Set whether to crossfade the next track
* Used when the engine is switching tracks automatically
* instead of manually.
*/
void setXFadeNextTrack( bool enable ) { m_xfadeNextTrack = enable; }
/** Set whether equalizer is enabled
* You don't need to cache the parameters, setEqualizerParameters is called straight after this
* function, _always_.
*/
virtual void setEqualizerEnabled( bool ) {};
/** Set equalizer parameters, all in range -100..100, where 0 = no adjustment
* @param preamp the preamplification value
* @param bandGains a list of 10 integers, ascending in frequency, the exact frequencies you amplify
* are not too-important at this time
*/
virtual void setEqualizerParameters( int /*preamp*/, const QList<int> &/*bandGains*/ ) {};
/** Tries to retrieve metadata for the given url (called only if url
* is not in the collection). The intended usage is to retrieve
* information for AudiCD tracks when they are added to the playlist
* (i.e. before they are actually played)
* @param url the url of the item
* @param bundle the SimpleMetaBundle to fill
* @return true if metadata found, false otherwise
*/
virtual bool metaDataForUrl(const QUrl &, Engine::SimpleMetaBundle &)
{ return false; }
/** returns true if this engine performs some special action to play
* audio cds: in this case, the KURL::List is filled with the urls of
* the songs in the cd...
*
* @param device the cdrom device , with QString::null meaning use engine-specific default value
* @param urls the list of urls for AudioCD tracks to fill
* @return true if the engine has the feature of reading from audio cds, false otherwise (note that this should return true also in case of error if the engine is capable of reading audio cds in general...)
* */
virtual bool getAudioCDContents(const QString &, QList<QUrl>&)
{ return false; }
/**
* Whether amarok_proxy.rb is needed for last.fm files.
* @return true if engine doesn't handle 'SYNC' messages in the stream from last.fm.
False if (like >=libxine-1.1.8) it does.
*/
virtual bool lastFmProxyRequired() { return true; }
/** flush the current stream buffer */
virtual bool flushBuffer() { return false; }
/** allow the engine to perform necessary work on changes in the playlist **/
virtual void playlistChanged() { };
virtual void reloadSettings() {};
protected:
Base();
/** Shows the Amarok configuration dialog at the engine page */
void showEngineConfigDialog() { emit showConfigDialog( "Engine" ); }
virtual void setVolumeSW( uint percent ) = 0;
/** Converts master volume to a logarithmic scale */
static uint makeVolumeLogarithmic( uint volume );
Base( const Base& ); //disable copy constructor
const Base &operator=( const Base& ); //disable copy constructor
int m_xfadeLength;
bool m_xfadeNextTrack;
protected:
static const int SCOPESIZE = 1024;
uint m_volume;
QUrl m_url;
Scope m_scope;
bool m_isStream;
};
class SimpleMetaBundle {
public:
QString title;
QString artist;
QString album;
QString comment;
QString genre;
QString bitrate;
QString samplerate;
QString length;
QString year;
QString tracknr;
};
}
#endif

242
src/fht.cpp Normal file
View File

@ -0,0 +1,242 @@
// 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);
}

119
src/fht.h Normal file
View File

@ -0,0 +1,119 @@
// 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

543
src/library.cpp Normal file
View File

@ -0,0 +1,543 @@
#include "library.h"
#include "librarybackend.h"
#include "libraryitem.h"
#include "songmimedata.h"
#include "libraryconfig.h"
#include <QStringList>
#include <QUrl>
#include <boost/bind.hpp>
Library::Library(EngineBase* engine, QObject* parent)
: QAbstractItemModel(parent),
engine_(engine),
backend_(new BackgroundThread<LibraryBackend>(this)),
watcher_(new BackgroundThread<LibraryWatcher>(this)),
root_(new LibraryItem(LibraryItem::Type_Root)),
artist_icon_(":artist.png"),
album_icon_(":album.png"),
config_(new LibraryConfig)
{
connect(backend_, SIGNAL(Initialised()), SLOT(BackendInitialised()));
connect(watcher_, SIGNAL(Initialised()), SLOT(WatcherInitialised()));
waiting_for_threads_ = 2;
}
Library::~Library() {
delete root_;
delete config_;
}
void Library::StartThreads() {
Q_ASSERT(waiting_for_threads_);
backend_->start();
watcher_->start();
}
void Library::BackendInitialised() {
connect(backend_->Worker().get(), SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
connect(backend_->Worker().get(), SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList)));
connect(backend_->Worker().get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
connect(backend_->Worker().get(), SIGNAL(TotalSongCountUpdated(int)), SIGNAL(TotalSongCountUpdated(int)));
config_->SetBackend(backend_->Worker());
if (--waiting_for_threads_ == 0)
Initialise();
}
void Library::WatcherInitialised() {
watcher_->Worker()->SetEngine(engine_);
if (--waiting_for_threads_ == 0)
Initialise();
}
void Library::Initialise() {
// The backend and watcher threads are finished initialising, now we can
// connect them together and start everything off.
watcher_->Worker()->SetBackend(backend_->Worker());
connect(backend_->Worker().get(), SIGNAL(DirectoriesDiscovered(DirectoryList)),
watcher_->Worker().get(), SLOT(AddDirectories(DirectoryList)));
connect(watcher_->Worker().get(), SIGNAL(NewOrUpdatedSongs(SongList)),
backend_->Worker().get(), SLOT(AddOrUpdateSongs(SongList)));
connect(watcher_->Worker().get(), SIGNAL(SongsMTimeUpdated(SongList)),
backend_->Worker().get(), SLOT(UpdateMTimesOnly(SongList)));
connect(watcher_->Worker().get(), SIGNAL(SongsDeleted(SongList)),
backend_->Worker().get(), SLOT(DeleteSongs(SongList)));
// This will start the watcher checking for updates
backend_->Worker()->LoadDirectoriesAsync();
backend_->Worker()->UpdateTotalSongCountAsync();
Reset();
}
bool Library::IsEmpty() const {
return root_->children.isEmpty();
}
void Library::SongsDiscovered(const SongList& songs) {
foreach (const Song& song, songs) {
LibraryItem* artist = NULL;
LibraryItem* album = NULL;
if (song.is_compilation()) {
if (compilation_artist_node_ == NULL)
CreateCompilationArtistNode(true);
artist = compilation_artist_node_;
} else {
if (artist_nodes_.contains(song.artist()))
artist = artist_nodes_[song.artist()];
else {
artist = CreateArtistNode(true, song.artist());
}
}
if (artist->lazy_loaded) {
album = artist->ChildByKey(song.album());
if (album == NULL)
album = CreateAlbumNode(true, song.album(), artist, song.is_compilation());
if (album->lazy_loaded)
CreateSongNode(true, song, album);
}
}
}
LibraryItem* Library::CreateCompilationArtistNode(bool signal) {
LibraryItem* parent = root_;
if (signal)
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
compilation_artist_node_ =
new LibraryItem(LibraryItem::Type_CompilationArtist, "Various Artists", parent);
compilation_artist_node_->sort_text = " various";
if (signal)
endInsertRows();
return compilation_artist_node_;
}
LibraryItem* Library::CreateArtistNode(bool signal, const QString& name) {
LibraryItem* parent = root_;
if (signal)
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
LibraryItem* ret = new LibraryItem(LibraryItem::Type_Artist, name, parent);
ret->display_text = PrettyArtist(name);
ret->sort_text = SortTextForArtist(name);
artist_nodes_[name] = ret;
if (signal)
endInsertRows();
if (!name.isEmpty()) {
QChar divider_char = ret->sort_text[0];
if (!divider_nodes_.contains(divider_char)) {
if (signal)
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
LibraryItem* divider =
new LibraryItem(LibraryItem::Type_Divider, QString(divider_char), root_);
if (divider_char.isDigit())
divider->display_text = "0-9";
divider_nodes_[divider_char] = divider;
if (signal)
endInsertRows();
}
}
return ret;
}
LibraryItem* Library::CreateAlbumNode(bool signal, const QString& name, LibraryItem* parent, bool compilation) {
if (signal)
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
LibraryItem* ret = new LibraryItem(
compilation ? LibraryItem::Type_CompilationAlbum
: LibraryItem::Type_Album,
name, parent);
ret->display_text = PrettyAlbum(name);
ret->sort_text = SortTextForAlbum(name);
if (signal)
endInsertRows();
return ret;
}
LibraryItem* Library::CreateSongNode(bool signal, const Song& song, LibraryItem* parent) {
if (signal)
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
LibraryItem* ret = new LibraryItem(LibraryItem::Type_Song, song.title(), parent);
ret->display_text = song.PrettyTitleWithArtist();
ret->song = song;
song_nodes_[song.id()] = ret;
if (signal)
endInsertRows();
return ret;
}
void Library::SongsDeleted(const SongList& songs) {
// Delete song nodes
foreach (const Song& song, songs) {
if (song_nodes_.contains(song.id())) {
LibraryItem* node = song_nodes_[song.id()];
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
node->parent->Delete(node->row);
song_nodes_.remove(song.id());
endRemoveRows();
}
}
// Delete now-empty album nodes
foreach (const Song& song, songs) {
LibraryItem* artist = NULL;
if (song.is_compilation() && compilation_artist_node_->lazy_loaded)
artist = compilation_artist_node_;
else if (artist_nodes_.contains(song.artist()) &&
artist_nodes_[song.artist()]->lazy_loaded)
artist = artist_nodes_[song.artist()];
if (artist == NULL)
continue;
LibraryItem* node = artist->ChildByKey(song.album());
if (!node)
continue;
LazyPopulate(node);
if (node->children.count() == 0) {
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
node->parent->Delete(node->row);
endRemoveRows();
}
}
// Delete now-empty artist nodes
foreach (const Song& song, songs) {
// Was it a compilation?
LibraryItem* node = NULL;
if (song.is_compilation())
node = compilation_artist_node_;
else
node = artist_nodes_.contains(song.artist()) ? artist_nodes_[song.artist()] : NULL;
if (node) {
LazyPopulate(node);
if (node->children.count() == 0) {
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
node->parent->Delete(node->row);
if (song.is_compilation())
compilation_artist_node_ = NULL;
else {
artist_nodes_.remove(song.artist());
// Delete now-empty dividers
QString sort_text(SortTextForArtist(song.artist()));
if (!sort_text.isEmpty() && divider_nodes_.contains(sort_text[0])) {
QChar c(sort_text[0]);
bool found = false;
foreach (LibraryItem* artist_node, artist_nodes_.values()) {
if (artist_node->sort_text.startsWith(c)) {
found = true;
break;
}
}
if (!found) {
root_->Delete(divider_nodes_[c]->row);
divider_nodes_.remove(c);
}
}
}
endRemoveRows();
}
}
}
}
LibraryItem* Library::IndexToItem(const QModelIndex& index) const {
if (!index.isValid())
return root_;
return reinterpret_cast<LibraryItem*>(index.internalPointer());
}
QModelIndex Library::ItemToIndex(LibraryItem* item) const {
if (!item || !item->parent)
return QModelIndex();
return createIndex(item->row, 0, item);
}
int Library::columnCount(const QModelIndex &) const {
return 1;
}
QVariant Library::data(const QModelIndex& index, int role) const {
const LibraryItem* item = IndexToItem(index);
return data(item, role);
}
QVariant Library::data(const LibraryItem* item, int role) const {
switch (role) {
case Qt::DisplayRole:
return item->DisplayText();
case Qt::DecorationRole:
switch (item->type) {
case LibraryItem::Type_Album:
case LibraryItem::Type_CompilationAlbum:
return album_icon_;
case LibraryItem::Type_Artist:
case LibraryItem::Type_CompilationArtist:
return artist_icon_;
default:
break;
}
break;
case Role_Type:
return item->type;
case Role_Key:
return item->key;
case Role_SortText:
if (item->type == LibraryItem::Type_Song)
return item->song.disc() * 1000 + item->song.track();
return item->SortText();
}
return QVariant();
}
QModelIndex Library::index(int row, int, const QModelIndex& parent) const {
LibraryItem* parent_item = IndexToItem(parent);
if (!parent_item || parent_item->children.count() <= row)
return QModelIndex();
return ItemToIndex(parent_item->children[row]);
}
QModelIndex Library::parent(const QModelIndex& index) const {
return ItemToIndex(IndexToItem(index)->parent);
}
int Library::rowCount(const QModelIndex & parent) const {
LibraryItem* item = IndexToItem(parent);
const_cast<Library*>(this)->LazyPopulate(item); // Ahem
return item->children.count();
}
bool Library::hasChildren(const QModelIndex &parent) const {
LibraryItem* item = IndexToItem(parent);
if (item->lazy_loaded)
return !item->children.isEmpty();
else
return true;
}
void Library::LazyPopulate(LibraryItem* item) {
if (item->lazy_loaded)
return;
switch (item->type) {
case LibraryItem::Type_CompilationArtist:
foreach (const QString& album, backend_->Worker()->GetCompilationAlbums(query_options_))
CreateAlbumNode(false, album, item, true);
break;
case LibraryItem::Type_CompilationAlbum:
foreach (const Song& song, backend_->Worker()->GetCompilationSongs(query_options_, item->key))
CreateSongNode(false, song, item);
break;
case LibraryItem::Type_Artist:
foreach (const QString& album, backend_->Worker()->GetAlbumsByArtist(query_options_, item->key))
CreateAlbumNode(false, album, item, false);
break;
case LibraryItem::Type_Album:
foreach (const Song& song, backend_->Worker()->GetSongs(query_options_, item->parent->key, item->key))
CreateSongNode(false, song, item);
break;
default:
qWarning("Tried to LazyPopulate a bad item type");
break;
}
item->lazy_loaded = true;
}
void Library::Reset() {
delete root_;
artist_nodes_.clear();
song_nodes_.clear();
divider_nodes_.clear();
compilation_artist_node_ = NULL;
root_ = new LibraryItem(LibraryItem::Type_Root);
// Various artists?
if (backend_->Worker()->HasCompilations(query_options_))
CreateCompilationArtistNode(false);
// Populate artists
foreach (const QString& artist, backend_->Worker()->GetAllArtists(query_options_))
CreateArtistNode(false, artist);
reset();
}
QString Library::PrettyArtist(QString artist) const {
if (artist.isEmpty()) {
artist = "Unknown";
}
return artist;
}
QString Library::SortTextForArtist(QString artist) const {
artist = SortTextForAlbum(artist);
if (artist.startsWith("the ")) {
artist = artist.right(artist.length() - 4) + ", the";
}
return artist;
}
QString Library::PrettyAlbum(QString album) const {
if (album.isEmpty()) {
album = "Unknown";
}
return album;
}
QString Library::SortTextForAlbum(QString album) const {
if (album.isEmpty()) {
album = " unknown";
} else {
album = album.toLower();
}
album = album.remove(QRegExp("[^\\w ]"));
return album;
}
Qt::ItemFlags Library::flags(const QModelIndex& index) const {
switch (IndexToItem(index)->type) {
case LibraryItem::Type_Album:
case LibraryItem::Type_Artist:
case LibraryItem::Type_Song:
case LibraryItem::Type_CompilationAlbum:
case LibraryItem::Type_CompilationArtist:
return Qt::ItemIsSelectable |
Qt::ItemIsEnabled |
Qt::ItemIsDragEnabled;
case LibraryItem::Type_Divider:
case LibraryItem::Type_Root:
default:
return Qt::ItemIsEnabled;
}
}
QStringList Library::mimeTypes() const {
return QStringList() << "text/uri-list";
}
QMimeData* Library::mimeData(const QModelIndexList& indexes) const {
SongMimeData* data = new SongMimeData;
QList<QUrl> urls;
foreach (const QModelIndex& index, indexes) {
GetChildSongs(IndexToItem(index), &urls, &data->songs);
}
data->setUrls(urls);
return data;
}
bool Library::CompareItems(const LibraryItem* a, const LibraryItem* b) const {
return data(a, Library::Role_SortText).toString() <
data(b, Library::Role_SortText).toString();
}
void Library::GetChildSongs(LibraryItem* item, QList<QUrl>* urls, SongList* songs) const {
switch (item->type) {
case LibraryItem::Type_Album:
case LibraryItem::Type_Artist:
case LibraryItem::Type_CompilationAlbum:
case LibraryItem::Type_CompilationArtist: {
const_cast<Library*>(this)->LazyPopulate(item);
QList<LibraryItem*> children = item->children;
qSort(children.begin(), children.end(), boost::bind(
&Library::CompareItems, this, _1, _2));
foreach (LibraryItem* child, children)
GetChildSongs(child, urls, songs);
break;
}
case LibraryItem::Type_Song:
urls->append(QUrl::fromLocalFile(item->song.filename()));
songs->append(item->song);
break;
default:
break;
}
}
SongList Library::GetChildSongs(const QModelIndex& index) const {
QList<QUrl> dontcare;
SongList ret;
if (!index.isValid())
return SongList();
GetChildSongs(IndexToItem(index), &dontcare, &ret);
return ret;
}
void Library::ShowConfig() {
config_->show();
}
void Library::SetFilterAge(int age) {
query_options_.max_age = age;
Reset();
}
void Library::SetFilterText(const QString& text) {
query_options_.filter = text;
Reset();
}

110
src/library.h Normal file
View File

@ -0,0 +1,110 @@
#ifndef LIBRARY_H
#define LIBRARY_H
#include <QAbstractItemModel>
#include <QIcon>
#include "backgroundthread.h"
#include "librarybackend.h"
#include "librarywatcher.h"
#include "libraryquery.h"
#include "engine_fwd.h"
#include "song.h"
class LibraryItem;
class LibraryConfig;
class Library : public QAbstractItemModel {
Q_OBJECT
public:
Library(EngineBase* engine, QObject* parent = 0);
~Library();
enum {
Role_Type = Qt::UserRole + 1,
Role_SortText,
Role_Key,
};
void StartThreads();
void GetChildSongs(LibraryItem* item, QList<QUrl>* urls, SongList* songs) const;
SongList GetChildSongs(const QModelIndex& index) const;
bool IsEmpty() const;
// QAbstractItemModel
int columnCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex & index) const;
int rowCount(const QModelIndex & parent = QModelIndex()) const;
bool hasChildren(const QModelIndex &parent) const;
Qt::ItemFlags flags(const QModelIndex& index) const;
QStringList mimeTypes() const;
QMimeData* mimeData(const QModelIndexList& indexes) const;
signals:
void Error(const QString& message);
void TotalSongCountUpdated(int count);
public slots:
void ShowConfig();
void SetFilterAge(int age);
void SetFilterText(const QString& text);
private slots:
// From LibraryBackend
void BackendInitialised();
void SongsDiscovered(const SongList& songs);
void SongsDeleted(const SongList& songs);
void Reset();
// From LibraryWatcher
void WatcherInitialised();
private:
void Initialise();
void LazyPopulate(LibraryItem* item);
LibraryItem* CreateCompilationArtistNode(bool signal);
LibraryItem* CreateArtistNode(bool signal, const QString& name);
LibraryItem* CreateAlbumNode(bool signal, const QString& name, LibraryItem* parent, bool compilation);
LibraryItem* CreateSongNode(bool signal, const Song& song, LibraryItem* parent);
QString PrettyArtist(QString artist) const;
QString PrettyAlbum(QString album) const;
QString SortTextForArtist(QString artist) const;
QString SortTextForAlbum(QString album) const;
LibraryItem* IndexToItem(const QModelIndex& index) const;
QModelIndex ItemToIndex(LibraryItem* item) const;
QVariant data(const LibraryItem* item, int role) const;
bool CompareItems(const LibraryItem* a, const LibraryItem* b) const;
private:
EngineBase* engine_;
BackgroundThread<LibraryBackend>* backend_;
BackgroundThread<LibraryWatcher>* watcher_;
int waiting_for_threads_;
QueryOptions query_options_;
LibraryItem* root_;
QMap<int, LibraryItem*> song_nodes_;
QMap<QString, LibraryItem*> artist_nodes_;
QMap<QChar, LibraryItem*> divider_nodes_;
LibraryItem* compilation_artist_node_;
QIcon artist_icon_;
QIcon album_icon_;
LibraryConfig* config_;
};
#endif // LIBRARY_H

382
src/librarybackend.cpp Normal file
View File

@ -0,0 +1,382 @@
#include "librarybackend.h"
#include "libraryquery.h"
#include <QFile>
#include <QDir>
#include <QVariant>
#include <QSettings>
#include <QtDebug>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QCoreApplication>
#include <QThread>
const char* LibraryBackend::kDatabaseName = "tangerine.db";
LibraryBackend::LibraryBackend(QObject* parent)
: QObject(parent)
{
QSettings s;
s.beginGroup("Library");
directory_ = s.value("database_directory", DefaultDirectory()).toString();
Connect();
}
QString LibraryBackend::DefaultDirectory() {
QDir ret(QDir::homePath() + "/.config/" + QCoreApplication::organizationName());
return QDir::toNativeSeparators(ret.path());
}
QSqlDatabase LibraryBackend::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("thread_" + QString::number(
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);
db.setDatabaseName(directory_ + "/" + kDatabaseName);
if (!db.open()) {
emit Error(db.lastError().text());
return db;
}
if (db.tables().count() == 0) {
// Set up initial schema
QFile schema_file(":/schema.sql");
schema_file.open(QIODevice::ReadOnly);
QString schema(QString::fromUtf8(schema_file.readAll()));
QStringList commands(schema.split(";\n\n"));
foreach (const QString& command, commands) {
QSqlQuery query(db.exec(command));
if (CheckErrors(query.lastError())) return db;
}
}
return db;
}
bool LibraryBackend::CheckErrors(const QSqlError& error) {
if (error.isValid()) {
qDebug() << error;
emit Error(error.text());
return true;
}
return false;
}
void LibraryBackend::LoadDirectoriesAsync() {
metaObject()->invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
}
void LibraryBackend::UpdateTotalSongCountAsync() {
metaObject()->invokeMethod(this, "UpdateTotalSongCount", Qt::QueuedConnection);
}
void LibraryBackend::LoadDirectories() {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT ROWID, path"
" FROM directories", db);
q.exec();
if (CheckErrors(q.lastError())) return;
DirectoryList directories;
while (q.next()) {
Directory dir;
dir.id = q.value(0).toInt();
dir.path = q.value(1).toString();
directories << dir;
}
emit DirectoriesDiscovered(directories);
}
void LibraryBackend::UpdateTotalSongCount() {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT COUNT(*) FROM songs", db);
q.exec();
if (CheckErrors(q.lastError())) return;
if (!q.next()) return;
emit TotalSongCountUpdated(q.value(0).toInt());
}
void LibraryBackend::AddDirectory(const QString &path) {
QSqlDatabase db(Connect());
QSqlQuery q("INSERT INTO directories (path, subdirs)"
" VALUES (:path, 1)", db);
q.bindValue(":path", path);
q.exec();
if (CheckErrors(q.lastError())) return;
Directory dir;
dir.path = path;
dir.id = q.lastInsertId().toInt();
emit DirectoriesDiscovered(DirectoryList() << dir);
}
void LibraryBackend::RemoveDirectory(const Directory& dir) {
QSqlDatabase db(Connect());
// Remove songs first
DeleteSongs(FindSongsInDirectory(dir.id));
// Now remove the directory
QSqlQuery q("DELETE FROM directories WHERE ROWID = :id", db);
q.bindValue(":id", dir.id);
q.exec();
if (CheckErrors(q.lastError())) return;
emit DirectoriesDeleted(DirectoryList() << dir);
}
SongList LibraryBackend::FindSongsInDirectory(int id) {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT ROWID, " + QString(Song::kColumnSpec) +
" FROM songs WHERE directory = :directory", db);
q.bindValue(":directory", id);
q.exec();
if (CheckErrors(q.lastError())) return SongList();
SongList ret;
while (q.next()) {
Song song;
song.InitFromQuery(q);
ret << song;
}
return ret;
}
void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
QSqlDatabase db(Connect());
QSqlQuery check_dir(
"SELECT ROWID FROM directories WHERE ROWID = :id", db);
QSqlQuery add_song(
"INSERT INTO songs (" + QString(Song::kColumnSpec) + ")"
" VALUES (" + QString(Song::kBindSpec) + ")", db);
QSqlQuery update_song(
"UPDATE songs SET " + QString(Song::kUpdateSpec) +
" WHERE ROWID = :id", db);
db.transaction();
SongList added_songs;
SongList deleted_songs;
foreach (const Song& song, songs) {
// Do a sanity check first - make sure the song's directory still exists
// This is to fix a possible race condition when a directory is removed
// while LibraryWatcher is scanning it.
check_dir.bindValue(":id", song.directory_id());
check_dir.exec();
if (CheckErrors(check_dir.lastError())) continue;
if (!check_dir.next())
continue; // Directory didn't exist
if (song.id() == -1) {
// Create
song.BindToQuery(&add_song);
add_song.exec();
if (CheckErrors(add_song.lastError())) continue;
Song copy(song);
copy.set_id(add_song.lastInsertId().toInt());
added_songs << copy;
} else {
// Get the previous song data first
Song old_song(GetSongById(song.id()));
if (!old_song.is_valid())
continue;
// Update
song.BindToQuery(&update_song);
update_song.bindValue(":id", song.id());
update_song.exec();
if (CheckErrors(update_song.lastError())) continue;
deleted_songs << old_song;
added_songs << song;
}
}
db.commit();
if (!deleted_songs.isEmpty())
emit SongsDeleted(deleted_songs);
if (!added_songs.isEmpty())
emit SongsDiscovered(added_songs);
UpdateTotalSongCountAsync();
}
void LibraryBackend::UpdateMTimesOnly(const SongList& songs) {
QSqlDatabase db(Connect());
QSqlQuery q("UPDATE songs SET mtime = :mtime WHERE ROWID = :id", db);
db.transaction();
foreach (const Song& song, songs) {
q.bindValue(":mtime", song.mtime());
q.bindValue(":id", song.id());
q.exec();
CheckErrors(q.lastError());
}
db.commit();
}
void LibraryBackend::DeleteSongs(const SongList &songs) {
QSqlDatabase db(Connect());
QSqlQuery q("DELETE FROM songs WHERE ROWID = :id", db);
db.transaction();
foreach (const Song& song, songs) {
q.bindValue(":id", song.id());
q.exec();
CheckErrors(q.lastError());
}
db.commit();
emit SongsDeleted(songs);
UpdateTotalSongCountAsync();
}
QStringList LibraryBackend::GetAllArtists(const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT artist");
query.AddWhere("compilation", 0);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return QStringList();
QStringList ret;
while (q.next()) {
ret << q.value(0).toString();
}
return ret;
}
QStringList LibraryBackend::GetAlbumsByArtist(const QueryOptions& opt, const QString& artist) {
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT album");
query.AddWhere("compilation", 0);
query.AddWhere("artist", artist);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return QStringList();
QStringList ret;
while (q.next()) {
ret << q.value(0).toString();
}
return ret;
}
SongList LibraryBackend::GetSongs(const QueryOptions& opt, const QString& artist, const QString& album) {
LibraryQuery query(opt);
query.SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
query.AddWhere("compilation", 0);
query.AddWhere("artist", artist);
query.AddWhere("album", album);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return SongList();
SongList ret;
while (q.next()) {
Song song;
song.InitFromQuery(q);
ret << song;
}
return ret;
}
Song LibraryBackend::GetSongById(int id) {
QSqlDatabase db(Connect());
QSqlQuery q("SELECT ROWID, " + QString(Song::kColumnSpec) + " FROM songs"
" WHERE ROWID = :id", db);
q.bindValue(":id", id);
q.exec();
if (CheckErrors(q.lastError())) return Song();
q.next();
Song ret;
ret.InitFromQuery(q);
return ret;
}
bool LibraryBackend::HasCompilations(const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("ROWID");
query.AddWhere("compilation", 1);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return false;
return q.next();
}
QStringList LibraryBackend::GetCompilationAlbums(const QueryOptions& opt) {
LibraryQuery query(opt);
query.SetColumnSpec("DISTINCT album");
query.AddWhere("compilation", 1);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return QStringList();
QStringList ret;
while (q.next()) {
ret << q.value(0).toString();
}
return ret;
}
SongList LibraryBackend::GetCompilationSongs(const QueryOptions& opt, const QString& album) {
LibraryQuery query(opt);
query.SetColumnSpec("ROWID, " + QString(Song::kColumnSpec));
query.AddWhere("compilation", 1);
query.AddWhere("album", album);
QSqlQuery q(query.Query(Connect()));
q.exec();
if (CheckErrors(q.lastError())) return SongList();
SongList ret;
while (q.next()) {
Song song;
song.InitFromQuery(q);
ret << song;
}
return ret;
}

72
src/librarybackend.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef LIBRARYBACKEND_H
#define LIBRARYBACKEND_H
#include <QObject>
#include <QSqlError>
#include <QSqlDatabase>
#include <QMutex>
#include "directory.h"
#include "song.h"
struct QueryOptions;
class LibraryBackend : public QObject {
Q_OBJECT
public:
LibraryBackend(QObject* parent = 0);
// This actually refers to the location of the sqlite database
static QString DefaultDirectory();
// Get a list of directories in the library. Emits DirectoriesDiscovered.
void LoadDirectoriesAsync();
// Counts the songs in the library. Emits TotalSongCountUpdated
void UpdateTotalSongCountAsync();
SongList FindSongsInDirectory(int id);
QStringList GetAllArtists(const QueryOptions& opt);
QStringList GetAlbumsByArtist(const QueryOptions& opt, const QString& artist);
SongList GetSongs(const QueryOptions& opt, const QString& artist, const QString& album);
bool HasCompilations(const QueryOptions& opt);
QStringList GetCompilationAlbums(const QueryOptions& opt);
SongList GetCompilationSongs(const QueryOptions& opt, const QString& album);
Song GetSongById(int id);
void AddDirectory(const QString& path);
void RemoveDirectory(const Directory& dir);
public slots:
void LoadDirectories();
void UpdateTotalSongCount();
void AddOrUpdateSongs(const SongList& songs);
void UpdateMTimesOnly(const SongList& songs);
void DeleteSongs(const SongList& songs);
signals:
void Error(const QString& message);
void DirectoriesDiscovered(const DirectoryList& directories);
void DirectoriesDeleted(const DirectoryList& directories);
void SongsDiscovered(const SongList& songs);
void SongsDeleted(const SongList& songs);
void TotalSongCountUpdated(int total);
private:
QSqlDatabase Connect();
bool CheckErrors(const QSqlError& error);
private:
static const char* kDatabaseName;
QString directory_;
QMutex connect_mutex_;
};
#endif // LIBRARYBACKEND_H

76
src/libraryconfig.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "libraryconfig.h"
#include "librarybackend.h"
#include <QFileDialog>
#include <QSettings>
#include <QDir>
const char* LibraryConfig::kSettingsGroup = "LibraryConfig";
LibraryConfig::LibraryConfig(QWidget* parent)
: QDialog(parent),
dir_icon_(":folder.png")
{
ui_.setupUi(this);
connect(ui_.add, SIGNAL(clicked()), SLOT(Add()));
connect(ui_.remove, SIGNAL(clicked()), SLOT(Remove()));
connect(ui_.list, SIGNAL(currentRowChanged(int)), SLOT(CurrentRowChanged(int)));
}
void LibraryConfig::SetBackend(boost::shared_ptr<LibraryBackend> backend) {
backend_ = backend;
connect(backend_.get(), SIGNAL(DirectoriesDiscovered(DirectoryList)), SLOT(DirectoriesDiscovered(DirectoryList)));
connect(backend_.get(), SIGNAL(DirectoriesDeleted(DirectoryList)), SLOT(DirectoriesDeleted(DirectoryList)));
ui_.list->setEnabled(true);
ui_.add->setEnabled(true);
}
void LibraryConfig::Add() {
QSettings settings;
settings.beginGroup(kSettingsGroup);
QString path(settings.value("last_path", QDir::homePath()).toString());
path = QFileDialog::getExistingDirectory(this, "Add directory...", path);
if (!path.isNull()) {
backend_->AddDirectory(path);
}
settings.setValue("last_path", path);
}
void LibraryConfig::Remove() {
QListWidgetItem* item = ui_.list->currentItem();
if (item == NULL)
return;
Directory dir;
dir.path = item->text();
dir.id = item->type();
backend_->RemoveDirectory(dir);
}
void LibraryConfig::DirectoriesDiscovered(const DirectoryList& list) {
foreach (const Directory& dir, list) {
new QListWidgetItem(dir_icon_, dir.path, ui_.list, dir.id);
}
}
void LibraryConfig::DirectoriesDeleted(const DirectoryList& list) {
foreach (const Directory& dir, list) {
for (int i=0 ; i<ui_.list->count() ; ++i) {
if (ui_.list->item(i)->type() == dir.id) {
delete ui_.list->takeItem(i);
break;
}
}
}
}
void LibraryConfig::CurrentRowChanged(int row) {
ui_.remove->setEnabled(row != -1);
}

39
src/libraryconfig.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef LIBRARYCONFIG_H
#define LIBRARYCONFIG_H
#include <QDialog>
#include <boost/shared_ptr.hpp>
#include "ui_libraryconfig.h"
#include "directory.h"
class LibraryBackend;
class LibraryConfig : public QDialog {
Q_OBJECT
public:
LibraryConfig(QWidget* parent = 0);
void SetBackend(boost::shared_ptr<LibraryBackend> backend);
private slots:
void Add();
void Remove();
void DirectoriesDiscovered(const DirectoryList&);
void DirectoriesDeleted(const DirectoryList&);
void CurrentRowChanged(int);
private:
static const char* kSettingsGroup;
Ui::LibraryConfig ui_;
QIcon dir_icon_;
boost::shared_ptr<LibraryBackend> backend_;
};
#endif // LIBRARYCONFIG_H

146
src/libraryconfig.ui Normal file
View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LibraryConfig</class>
<widget class="QDialog" name="LibraryConfig">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>489</width>
<height>273</height>
</rect>
</property>
<property name="windowTitle">
<string>Music Library</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>These folders will be scanned for music to make up your library</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="list">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="add">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Add new folder...</string>
</property>
<property name="icon">
<iconset resource="../data/data.qrc">
<normaloff>:/folder-new.png</normaloff>:/folder-new.png</iconset>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove folder</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</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::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>list</tabstop>
<tabstop>add</tabstop>
<tabstop>remove</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources>
<include location="../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LibraryConfig</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>272</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LibraryConfig</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>272</y>
</hint>
</hints>
</connection>
</connections>
</ui>

35
src/libraryitem.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "libraryitem.h"
LibraryItem::LibraryItem(Type _type, const QString& _key, LibraryItem* _parent)
: type(_type),
key(_key),
lazy_loaded(_type == Type_Song || _type == Type_Root || _type == Type_Divider),
parent(_parent)
{
if (parent) {
row = parent->children.count();
parent->children << this;
} else {
row = 0;
}
}
LibraryItem::~LibraryItem() {
qDeleteAll(children);
}
void LibraryItem::Delete(int child_row) {
delete children.takeAt(child_row);
// Adjust row numbers of those below it :(
for (int i=child_row ; i<children.count() ; ++i)
children[i]->row --;
}
LibraryItem* LibraryItem::ChildByKey(const QString& key) const {
foreach (LibraryItem* child, children) {
if (child->key == key)
return child;
}
return NULL;
}

41
src/libraryitem.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef LIBRARYITEM_H
#define LIBRARYITEM_H
#include <QString>
#include <QList>
#include "song.h"
struct LibraryItem {
enum Type {
Type_Root,
Type_Divider,
Type_CompilationArtist,
Type_CompilationAlbum,
Type_Artist,
Type_Album,
Type_Song,
};
LibraryItem(Type _type, const QString& _key = QString::null, LibraryItem* _parent = NULL);
~LibraryItem();
void Delete(int child_row);
LibraryItem* ChildByKey(const QString& key) const;
QString DisplayText() const { return display_text.isNull() ? key : display_text; }
QString SortText() const { return sort_text.isNull() ? key : sort_text; }
Type type;
QString key;
QString sort_text;
QString display_text;
int row;
bool lazy_loaded;
Song song;
LibraryItem* parent;
QList<LibraryItem*> children;
};
#endif // LIBRARYITEM_H

56
src/libraryquery.cpp Normal file
View File

@ -0,0 +1,56 @@
#include "libraryquery.h"
#include <QtDebug>
#include <QDateTime>
LibraryQuery::LibraryQuery()
{
}
LibraryQuery::LibraryQuery(const QueryOptions& options)
{
if (!options.filter.isEmpty()) {
where_clauses_ << "("
"artist LIKE ? OR "
"album LIKE ? OR "
"title LIKE ?)";
bound_values_ << "%" + options.filter + "%";
bound_values_ << "%" + options.filter + "%";
bound_values_ << "%" + options.filter + "%";
}
if (options.max_age != -1) {
int cutoff = QDateTime::currentDateTime().toTime_t() - options.max_age;
where_clauses_ << "ctime > ?";
bound_values_ << cutoff;
}
}
void LibraryQuery::AddWhere(const QString& column, const QVariant& value) {
// 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").arg(column, value.toString());
else {
where_clauses_ << QString("%1 = ?").arg(column);
bound_values_ << value;
}
}
QSqlQuery LibraryQuery::Query(QSqlDatabase db) const {
QString sql = QString("SELECT %1 FROM songs").arg(column_spec_);
if (!where_clauses_.isEmpty())
sql += " WHERE " + where_clauses_.join(" AND ");
QSqlQuery q(sql, db);
// Bind values
foreach (const QVariant& value, bound_values_) {
q.addBindValue(value);
}
return q;
}

33
src/libraryquery.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef LIBRARYQUERY_H
#define LIBRARYQUERY_H
#include <QString>
#include <QVariant>
#include <QSqlQuery>
#include <QStringList>
#include <QVariantList>
struct QueryOptions {
QueryOptions() : max_age(-1) {}
QString filter;
int max_age;
};
class LibraryQuery {
public:
LibraryQuery();
LibraryQuery(const QueryOptions& options);
void SetColumnSpec(const QString& spec) { column_spec_ = spec; }
void AddWhere(const QString& column, const QVariant& value);
QSqlQuery Query(QSqlDatabase db) const;
private:
QString column_spec_;
QStringList where_clauses_;
QVariantList bound_values_;
};
#endif // LIBRARYQUERY_H

139
src/libraryview.cpp Normal file
View File

@ -0,0 +1,139 @@
#include "library.h"
#include "libraryview.h"
#include "libraryitem.h"
#include <QPainter>
const int LibraryView::kRowsToShow = 50;
LibraryItemDelegate::LibraryItemDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void LibraryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const {
LibraryItem::Type type =
static_cast<LibraryItem::Type>(index.data(Library::Role_Type).toInt());
switch (type) {
case LibraryItem::Type_Divider: {
QString text(index.data().toString().toUpper());
// Draw the background
//QStyledItemDelegate::paint(painter, opt, QModelIndex());
painter->save();
// Draw the text
QFont bold_font(opt.font);
bold_font.setBold(true);
QRect text_rect(opt.rect);
text_rect.setLeft(text_rect.left() + 30);
painter->setPen(opt.palette.color(QPalette::Text));
painter->setFont(bold_font);
painter->drawText(text_rect, text);
//Draw the line under the item
QPen line_pen(opt.palette.color(QPalette::Disabled, QPalette::Text).lighter());
line_pen.setWidth(2);
painter->setPen(line_pen);
painter->drawLine(opt.rect.bottomLeft(), opt.rect.bottomRight());
painter->restore();
break;
}
default:
QStyledItemDelegate::paint(painter, opt, index);
break;
}
}
LibraryView::LibraryView(QWidget* parent)
: QTreeView(parent),
library_(NULL),
total_song_count_(-1),
nomusic_(":nomusic.png")
{
setItemDelegate(new LibraryItemDelegate(this));
}
void LibraryView::SetLibrary(Library *library) {
library_ = library;
}
void LibraryView::TotalSongCountUpdated(int 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();
}
void LibraryView::reset() {
QTreeView::reset();
// Expand nodes in the tree until we have about 50 rows visible in the view
int rows = model()->rowCount(rootIndex());
RecursivelyExpand(rootIndex(), &rows);
}
void LibraryView::paintEvent(QPaintEvent* event) {
QTreeView::paintEvent(event);
QPainter p(viewport());
QRect rect(viewport()->rect());
if (total_song_count_ == 0) {
// Draw the confused tangerine
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, "Your library 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, "Click here to add some music");
}
}
void LibraryView::mouseReleaseEvent(QMouseEvent* e) {
QTreeView::mouseReleaseEvent(e);
if (total_song_count_ == 0) {
emit ShowConfigDialog();
}
}
bool LibraryView::RecursivelyExpand(const QModelIndex& index, int* count) {
int children = model()->rowCount(index);
if (*count + children > kRowsToShow)
return false;
expand(index);
*count += children;
for (int i=0 ; i<children ; ++i) {
if (!RecursivelyExpand(model()->index(i, 0, index), count))
return false;
}
return true;
}

50
src/libraryview.h Normal file
View File

@ -0,0 +1,50 @@
#ifndef LIBRARYVIEW_H
#define LIBRARYVIEW_H
#include <QStyledItemDelegate>
#include <QTreeView>
class Library;
class LibraryItemDelegate : public QStyledItemDelegate {
public:
LibraryItemDelegate(QObject* parent);
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};
class LibraryView : public QTreeView {
Q_OBJECT
public:
LibraryView(QWidget* parent = 0);
void SetLibrary(Library* library);
public slots:
void TotalSongCountUpdated(int count);
signals:
void ShowConfigDialog();
protected:
// QAbstractItemView
void reset();
// QWidget
void paintEvent(QPaintEvent* event);
void mouseReleaseEvent(QMouseEvent* e);
private:
void RecheckIsEmpty();
bool RecursivelyExpand(const QModelIndex& index, int* count);
private:
static const int kRowsToShow;
Library* library_;
int total_song_count_;
QPixmap nomusic_;
};
#endif // LIBRARYVIEW_H

153
src/librarywatcher.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "librarywatcher.h"
#include "librarybackend.h"
#include "enginebase.h"
#include <QFileSystemWatcher>
#include <QDirIterator>
#include <QtDebug>
#include <QThread>
#include <QDateTime>
#include <QTimer>
#include <taglib/fileref.h>
#include <taglib/tag.h>
LibraryWatcher::LibraryWatcher(QObject* parent)
: QObject(parent),
fs_watcher_(new QFileSystemWatcher(this)),
rescan_timer_(new QTimer(this))
{
rescan_timer_->setInterval(1000);
rescan_timer_->setSingleShot(true);
connect(fs_watcher_, SIGNAL(directoryChanged(QString)), SLOT(DirectoryChanged(QString)));
connect(rescan_timer_, SIGNAL(timeout()), SLOT(RescanPathsNow()));
}
void LibraryWatcher::AddDirectories(const DirectoryList& directories) {
// Iterate through each directory to find a list of files that look like they
// could be music.
foreach (const Directory& dir, directories) {
paths_watched_[dir.path] = dir;
ScanDirectory(dir.path);
// Start monitoring this directory for more changes
fs_watcher_->addPath(dir.path);
}
}
void LibraryWatcher::RemoveDirectories(const DirectoryList &directories) {
foreach (const Directory& dir, directories) {
fs_watcher_->removePath(dir.path);
paths_watched_.remove(dir.path);
paths_needing_rescan_.removeAll(dir.path);
}
}
void LibraryWatcher::ScanDirectory(const QString& path) {
const Directory& dir = paths_watched_[path];
qDebug() << "Scanning" << path;
emit ScanStarted();
QStringList files_on_disk;
QDirIterator it(dir.path,
QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while (it.hasNext()) {
QString path(it.next());
// Don't bother if the engine can't decode it
if (!engine_->canDecode(QUrl::fromLocalFile(path)))
continue;
files_on_disk << path;
}
// Ask the database for a list of files in this directory
SongList songs_in_db = backend_->FindSongsInDirectory(dir.id);
// Now compare the list from the database with the list of files on disk
SongList new_songs;
SongList touched_songs;
foreach (const QString& file, files_on_disk) {
Song matching_song;
if (FindSongByPath(songs_in_db, file, &matching_song)) {
// The song is in the database and still on disk.
// Check the mtime to see if it's been changed since it was added.
if (matching_song.mtime() != QFileInfo(file).lastModified().toTime_t()) {
qDebug() << file << "changed";
// It's changed - reread the metadata from the file
Song song_on_disk;
song_on_disk.InitFromFile(file, dir.id);
song_on_disk.set_id(matching_song.id());
if (!matching_song.IsMetadataEqual(song_on_disk)) {
qDebug() << file << "metadata changed";
// Update the song in the DB
new_songs << song_on_disk;
} else {
// Only the metadata changed
touched_songs << song_on_disk;
}
}
} else {
// The song is on disk but not in the DB
Song song;
song.InitFromFile(file, dir.id);
if (!song.is_valid())
continue;
qDebug() << file << "created";
new_songs << song;
}
}
if (!new_songs.isEmpty())
emit NewOrUpdatedSongs(new_songs);
if (!touched_songs.isEmpty())
emit SongsMTimeUpdated(touched_songs);
// Look for deleted songs
SongList deleted_songs;
foreach (const Song& song, songs_in_db) {
if (!files_on_disk.contains(song.filename())) {
qDebug() << "Song deleted from disk:" << song.filename();
deleted_songs << song;
}
}
if (!deleted_songs.isEmpty())
emit SongsDeleted(deleted_songs);
qDebug() << "Finished scanning" << path;
emit ScanFinished();
}
bool LibraryWatcher::FindSongByPath(const SongList& list, const QString& path, Song* out) {
foreach (const Song& song, list) {
if (song.filename() == path) {
*out = song;
return true;
}
}
return false;
}
void LibraryWatcher::DirectoryChanged(const QString &path) {
if (!paths_needing_rescan_.contains(path))
paths_needing_rescan_ << path;
rescan_timer_->start();
}
void LibraryWatcher::RescanPathsNow() {
foreach (const QString& path, paths_needing_rescan_)
ScanDirectory(path);
paths_needing_rescan_.clear();
}

59
src/librarywatcher.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef LIBRARYWATCHER_H
#define LIBRARYWATCHER_H
#include "directory.h"
#include "song.h"
#include "engine_fwd.h"
#include <QObject>
#include <QStringList>
#include <QMap>
#include <boost/shared_ptr.hpp>
class QFileSystemWatcher;
class QTimer;
class LibraryBackend;
class LibraryWatcher : public QObject {
Q_OBJECT
public:
LibraryWatcher(QObject* parent = 0);
void SetBackend(boost::shared_ptr<LibraryBackend> backend) { backend_ = backend; }
void SetEngine(EngineBase* engine) { engine_ = engine; } // TODO: shared_ptr
signals:
void NewOrUpdatedSongs(const SongList& songs);
void SongsMTimeUpdated(const SongList& songs);
void SongsDeleted(const SongList& songs);
void ScanStarted();
void ScanFinished();
public slots:
void AddDirectories(const DirectoryList& directories);
void RemoveDirectories(const DirectoryList& directories);
private slots:
void DirectoryChanged(const QString& path);
void RescanPathsNow();
void ScanDirectory(const QString& path);
private:
static bool FindSongByPath(const SongList& list, const QString& path, Song* out);
private:
EngineBase* engine_;
boost::shared_ptr<LibraryBackend> backend_;
QFileSystemWatcher* fs_watcher_;
QTimer* rescan_timer_;
QMap<QString, Directory> paths_watched_;
QStringList paths_needing_rescan_;
};
#endif // LIBRARYWATCHER_H

20
src/main.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "mainwindow.h"
#include "directory.h"
#include "song.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QCoreApplication::setApplicationName("Tangerine");
QCoreApplication::setOrganizationName("Tangerine");
QCoreApplication::setOrganizationDomain("davidsansome.com");
qRegisterMetaType<DirectoryList>("DirectoryList");
qRegisterMetaType<SongList>("SongList");
QApplication a(argc, argv);
MainWindow w;
return a.exec();
}

349
src/mainwindow.cpp Normal file
View File

@ -0,0 +1,349 @@
#include "mainwindow.h"
#include "player.h"
#include "playlist.h"
#include "library.h"
#include "libraryconfig.h"
#include "songplaylistitem.h"
#include "systemtrayicon.h"
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <QUndoStack>
#include <QDir>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QtDebug>
#include <QCloseEvent>
#include <QSignalMapper>
const int MainWindow::kStateVersion = 1;
const char* MainWindow::kSettingsGroup = "MainWindow";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
playlist_(new Playlist(this)),
player_(new Player(playlist_, this)),
library_(new Library(player_->GetEngine(), this)),
library_sort_model_(new QSortFilterProxyModel(this)),
file_model_(new QFileSystemModel(this)),
file_undo_stack_(new QUndoStack(this)),
tray_icon_(new SystemTrayIcon(this))
{
ui_.setupUi(this);
tray_icon_->setIcon(windowIcon());
tray_icon_->setToolTip("Tangerine");
tray_icon_->show();
ui_.volume->setValue(player_->GetVolume());
// Models
library_sort_model_->setSourceModel(library_);
library_sort_model_->setSortRole(Library::Role_SortText);
library_sort_model_->setDynamicSortFilter(true);
library_sort_model_->sort(0);
playlist_->Restore();
ui_.playlist->setModel(playlist_);
ui_.library_view->setModel(library_sort_model_);
ui_.library_view->SetLibrary(library_);
// File browser
ui_.file_view->setModel(file_model_);
ChangeFilePathWithoutUndo(QDir::homePath());
connect(ui_.file_back, SIGNAL(clicked()), SLOT(FileBack()));
connect(ui_.file_forward, SIGNAL(clicked()), SLOT(FileForward()));
connect(ui_.file_home, SIGNAL(clicked()), SLOT(FileHome()));
connect(ui_.file_up, SIGNAL(clicked()), SLOT(FileUp()));
connect(ui_.file_path, SIGNAL(textChanged(QString)), SLOT(ChangeFilePath(QString)));
connect(file_undo_stack_, SIGNAL(canUndoChanged(bool)), ui_.file_back, SLOT(setEnabled(bool)));
connect(file_undo_stack_, SIGNAL(canRedoChanged(bool)), ui_.file_forward, SLOT(setEnabled(bool)));
connect(ui_.file_view, SIGNAL(activated(QModelIndex)), SLOT(FileClicked(QModelIndex)));
connect(ui_.file_view, SIGNAL(doubleClicked(QModelIndex)), SLOT(FileDoubleClicked(QModelIndex)));
// Action connections
connect(ui_.action_next_track, SIGNAL(triggered()), player_, SLOT(Next()));
connect(ui_.action_previous_track, SIGNAL(triggered()), player_, SLOT(Previous()));
connect(ui_.action_play_pause, SIGNAL(triggered()), player_, SLOT(PlayPause()));
connect(ui_.action_stop, SIGNAL(triggered()), player_, SLOT(Stop()));
connect(ui_.action_quit, SIGNAL(triggered()), qApp, SLOT(quit()));
connect(ui_.action_stop_after_this_track, SIGNAL(triggered()), SLOT(StopAfterCurrent()));
connect(ui_.library_filter, SIGNAL(textChanged(QString)), library_, SLOT(SetFilterText(QString)));
// Give actions to buttons
ui_.forward_button->setDefaultAction(ui_.action_next_track);
ui_.back_button->setDefaultAction(ui_.action_previous_track);
ui_.pause_play_button->setDefaultAction(ui_.action_play_pause);
ui_.stop_button->setDefaultAction(ui_.action_stop);
// Stop actions
QMenu* stop_menu = new QMenu(this);
stop_menu->addAction(ui_.action_stop);
stop_menu->addAction(ui_.action_stop_after_this_track);
ui_.stop_button->setMenu(stop_menu);
// Player connections
connect(ui_.volume, SIGNAL(valueChanged(int)), player_, SLOT(SetVolume(int)));
connect(player_, SIGNAL(Error(QString)), SLOT(ReportError(QString)));
connect(player_, SIGNAL(Paused()), SLOT(MediaPaused()));
connect(player_, SIGNAL(Playing()), SLOT(MediaPlaying()));
connect(player_, SIGNAL(Stopped()), SLOT(MediaStopped()));
connect(player_, SIGNAL(Paused()), playlist_, SLOT(Paused()));
connect(player_, SIGNAL(Playing()), playlist_, SLOT(Playing()));
connect(player_, SIGNAL(Stopped()), playlist_, SLOT(Stopped()));
connect(player_, SIGNAL(Paused()), ui_.playlist, SLOT(StopGlowing()));
connect(player_, SIGNAL(Playing()), ui_.playlist, SLOT(StartGlowing()));
connect(player_, SIGNAL(Stopped()), ui_.playlist, SLOT(StopGlowing()));
connect(ui_.playlist, SIGNAL(doubleClicked(QModelIndex)), SLOT(PlayIndex(QModelIndex)));
// Library connections
connect(library_, SIGNAL(Error(QString)), SLOT(ReportError(QString)));
connect(ui_.library_view, SIGNAL(doubleClicked(QModelIndex)), SLOT(LibraryDoubleClick(QModelIndex)));
connect(ui_.library_view, SIGNAL(ShowConfigDialog()), library_, SLOT(ShowConfig()));
connect(library_, SIGNAL(TotalSongCountUpdated(int)), ui_.library_view, SLOT(TotalSongCountUpdated(int)));
// Age filters
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_group->setExclusive(true);
QSignalMapper* 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()));
connect(filter_age_mapper, SIGNAL(mapped(int)), library_, SLOT(SetFilterAge(int)));
connect(ui_.library_filter_clear, SIGNAL(clicked()), SLOT(ClearLibraryFilter()));
// Library config menu
QMenu* library_menu = new QMenu(this);
library_menu->addActions(filter_age_group->actions());
library_menu->addSeparator();
library_menu->addAction("Configure library...", library_, SLOT(ShowConfig()));
ui_.library_options->setMenu(library_menu);
// Tray icon
QMenu* tray_menu = new QMenu(this);
tray_menu->addAction(ui_.action_previous_track);
tray_menu->addAction(ui_.action_play_pause);
tray_menu->addAction(ui_.action_stop);
tray_menu->addAction(ui_.action_next_track);
tray_menu->addSeparator();
tray_menu->addAction(ui_.action_quit);
tray_icon_->setContextMenu(tray_menu);
connect(tray_icon_, SIGNAL(WheelEvent(int)), SLOT(VolumeWheelEvent(int)));
connect(tray_icon_, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), SLOT(TrayClicked(QSystemTrayIcon::ActivationReason)));
// Analyzer
ui_.analyzer->set_engine(player_->GetEngine());
// Load theme
QFile stylesheet(":mainwindow.css");
if (!stylesheet.open(QIODevice::ReadOnly)) {
qWarning("Could not open stylesheet");
} else {
setStyleSheet(stylesheet.readAll());
}
// Load geometry
QSettings settings;
settings.beginGroup(kSettingsGroup);
restoreGeometry(settings.value("geometry").toByteArray());
if (!restoreState(settings.value("state").toByteArray(), kStateVersion)) {
tabifyDockWidget(ui_.files_dock, ui_.radio_dock);
tabifyDockWidget(ui_.files_dock, ui_.library_dock);
}
if (!settings.value("hidden", false).toBool())
show();
library_->StartThreads();
}
MainWindow::~MainWindow() {
SaveGeometry();
}
void MainWindow::FileUp() {
QDir dir(file_model_->rootDirectory());
dir.cdUp();
ChangeFilePath(dir.path());
}
void MainWindow::FileBack() {
QString new_path(file_undo_stack_->command(file_undo_stack_->index()-1)->text());
file_undo_stack_->undo();
ChangeFilePathWithoutUndo(new_path);
}
void MainWindow::FileForward() {
QString new_path(file_undo_stack_->command(file_undo_stack_->index()+1)->text());
file_undo_stack_->redo();
ChangeFilePathWithoutUndo(new_path);
}
void MainWindow::FileHome() {
ChangeFilePath(QDir::homePath());
}
void MainWindow::ChangeFilePath(const QString& new_path) {
QFileInfo info(new_path);
if (!info.exists() || !info.isDir())
return;
QString old_path(file_model_->rootPath());
ChangeFilePathWithoutUndo(new_path);
file_undo_stack_->push(new QUndoCommand(old_path));
}
void MainWindow::ChangeFilePathWithoutUndo(const QString& new_path) {
ui_.file_view->setRootIndex(file_model_->setRootPath(new_path));
ui_.file_path->setText(new_path);
QDir dir(new_path);
ui_.file_up->setEnabled(dir.cdUp());
}
void MainWindow::FileClicked(const QModelIndex& index) {
if (file_model_->isDir(index))
ChangeFilePath(file_model_->filePath(index));
}
void MainWindow::FileDoubleClicked(const QModelIndex& index) {
if (file_model_->isDir(index))
return;
Song song;
song.InitFromFile(file_model_->filePath(index), -1);
if (!song.is_valid())
return;
QModelIndex playlist_index = playlist_->InsertSongs(SongList() << song);
if (playlist_index.isValid() && player_->GetState() != Engine::Playing)
player_->PlayAt(playlist_index.row());
}
void MainWindow::ReportError(const QString& message) {
QMessageBox::warning(this, "Error", message);
}
void MainWindow::MediaStopped() {
ui_.action_stop->setEnabled(false);
ui_.action_stop_after_this_track->setEnabled(false);
ui_.action_play_pause->setIcon(QIcon(":media-playback-start.png"));
ui_.action_play_pause->setText("Play");
}
void MainWindow::MediaPaused() {
ui_.action_stop->setEnabled(true);
ui_.action_stop_after_this_track->setEnabled(true);
ui_.action_play_pause->setIcon(QIcon(":media-playback-start.png"));
ui_.action_play_pause->setText("Play");
}
void MainWindow::MediaPlaying() {
ui_.action_stop->setEnabled(true);
ui_.action_stop_after_this_track->setEnabled(true);
ui_.action_play_pause->setIcon(QIcon(":media-playback-pause.png"));
ui_.action_play_pause->setText("Pause");
}
void MainWindow::resizeEvent(QResizeEvent*) {
SaveGeometry();
}
void MainWindow::SaveGeometry() {
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.setValue("geometry", saveGeometry());
settings.setValue("state", saveState(kStateVersion));
}
void MainWindow::PlayIndex(const QModelIndex& index) {
if (!index.isValid())
return;
player_->PlayAt(index.row());
}
void MainWindow::LibraryDoubleClick(const QModelIndex& index) {
QModelIndex first_song =
playlist_->InsertSongs(library_->GetChildSongs(
library_sort_model_->mapToSource(index)));
if (first_song.isValid() && player_->GetState() != Engine::Playing)
player_->PlayAt(first_song.row());
}
void MainWindow::VolumeWheelEvent(int delta) {
ui_.volume->setValue(ui_.volume->value() + delta / 20);
}
void MainWindow::TrayClicked(QSystemTrayIcon::ActivationReason reason) {
switch (reason) {
case QSystemTrayIcon::DoubleClick:
case QSystemTrayIcon::Trigger:
SetHiddenInTray(isVisible());
break;
case QSystemTrayIcon::MiddleClick:
player_->PlayPause();
break;
default:
break;
}
}
void MainWindow::StopAfterCurrent() {
playlist_->StopAfter(playlist_->current_item());
}
void MainWindow::closeEvent(QCloseEvent* event) {
event->ignore();
SetHiddenInTray(true);
}
void MainWindow::SetHiddenInTray(bool hidden) {
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.setValue("hidden", hidden);
if (hidden)
hide();
else
show();
}
void MainWindow::ClearLibraryFilter() {
ui_.library_filter->clear();
ui_.library_filter->setFocus();
}

77
src/mainwindow.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSystemTrayIcon>
#include "ui_mainwindow.h"
class Playlist;
class Player;
class Library;
class LibraryConfig;
class QFileSystemModel;
class QSortFilterProxyModel;
class QUndoStack;
class SystemTrayIcon;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
void SetHiddenInTray(bool hidden);
protected:
void resizeEvent(QResizeEvent* event);
void closeEvent(QCloseEvent* event);
private slots:
void FileUp();
void FileBack();
void FileForward();
void FileHome();
void ChangeFilePath(const QString& new_path);
void FileClicked(const QModelIndex& index);
void FileDoubleClicked(const QModelIndex& index);
void ReportError(const QString& message);
void MediaStopped();
void MediaPaused();
void MediaPlaying();
void PlayIndex(const QModelIndex& index);
void StopAfterCurrent();
void LibraryDoubleClick(const QModelIndex& index);
void ClearLibraryFilter();
void VolumeWheelEvent(int delta);
void TrayClicked(QSystemTrayIcon::ActivationReason reason);
private:
void ChangeFilePathWithoutUndo(const QString& new_path);
void SaveGeometry();
private:
static const int kStateVersion;
static const char* kSettingsGroup;
Ui::MainWindow ui_;
Playlist* playlist_;
Player* player_;
Library* library_;
QSortFilterProxyModel* library_sort_model_;
QFileSystemModel* file_model_;
QUndoStack* file_undo_stack_;
SystemTrayIcon* tray_icon_;
};
#endif // MAINWINDOW_H

554
src/mainwindow.ui Normal file
View File

@ -0,0 +1,554 @@
<?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>804</width>
<height>559</height>
</rect>
</property>
<property name="windowTitle">
<string>Tangerine</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="PlaylistView" name="playlist">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
</widget>
</item>
<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="margin">
<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">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>30</height>
</size>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="BlockAnalyzer" 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="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>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<widget class="QDockWidget" name="library_dock">
<property name="features">
<set>QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Library</string>
</property>
<attribute name="dockWidgetArea">
<number>1</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_2">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="library_filter_clear">
<property name="icon">
<iconset>
<normaloff>:/clear.png</normaloff>:/clear.png</iconset>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="library_filter"/>
</item>
<item>
<widget class="QToolButton" name="library_options">
<property name="icon">
<iconset>
<normaloff>:/configure.png</normaloff>:/configure.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="LibraryView" name="library_view">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="radio_dock">
<property name="features">
<set>QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Radio</string>
</property>
<attribute name="dockWidgetArea">
<number>1</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_5"/>
</widget>
<widget class="QDockWidget" name="files_dock">
<property name="features">
<set>QDockWidget::DockWidgetMovable</set>
</property>
<property name="windowTitle">
<string>Files</string>
</property>
<attribute name="dockWidgetArea">
<number>1</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_6">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="file_back">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset>
<normaloff>:/go-previous.png</normaloff>:/go-previous.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="file_forward">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/go-next.png</normaloff>:/go-next.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="file_up">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/go-up.png</normaloff>:/go-up.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="file_home">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/go-home.png</normaloff>:/go-home.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="file_path"/>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="file_view"/>
</item>
</layout>
</widget>
</widget>
<action name="action_previous_track">
<property name="icon">
<iconset>
<normaloff>:/media-skip-backward.png</normaloff>:/media-skip-backward.png</iconset>
</property>
<property name="text">
<string>Previous track</string>
</property>
<property name="shortcut">
<string>Media Previous</string>
</property>
</action>
<action name="action_play_pause">
<property name="icon">
<iconset>
<normaloff>:/media-playback-start.png</normaloff>:/media-playback-start.png</iconset>
</property>
<property name="text">
<string>Play</string>
</property>
<property name="shortcut">
<string>Media Play</string>
</property>
</action>
<action name="action_stop">
<property name="enabled">
<bool>false</bool>
</property>
<property name="icon">
<iconset>
<normaloff>:/media-playback-stop.png</normaloff>:/media-playback-stop.png</iconset>
</property>
<property name="text">
<string>Stop</string>
</property>
<property name="shortcut">
<string>Media Stop</string>
</property>
</action>
<action name="action_next_track">
<property name="icon">
<iconset>
<normaloff>:/media-skip-forward.png</normaloff>:/media-skip-forward.png</iconset>
</property>
<property name="text">
<string>Next track</string>
</property>
<property name="shortcut">
<string>Media Next</string>
</property>
</action>
<action name="action_quit">
<property name="icon">
<iconset>
<normaloff>:/exit.png</normaloff>:/exit.png</iconset>
</property>
<property name="text">
<string>&amp;Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="action_stop_after_this_track">
<property name="icon">
<iconset>
<normaloff>:/media-playback-stop.png</normaloff>:/media-playback-stop.png</iconset>
</property>
<property name="text">
<string>Stop after this track</string>
</property>
</action>
<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>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>BlockAnalyzer</class>
<extends>QWidget</extends>
<header>analyzers/blockanalyzer.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>Amarok::VolumeSlider</class>
<extends>QSlider</extends>
<header>sliderwidget.h</header>
</customwidget>
<customwidget>
<class>PlaylistView</class>
<extends>QTreeView</extends>
<header>playlistview.h</header>
</customwidget>
<customwidget>
<class>LibraryView</class>
<extends>QTreeView</extends>
<header>libraryview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

103
src/player.cpp Normal file
View File

@ -0,0 +1,103 @@
#include "player.h"
#include "playlist.h"
#include "xine-engine.h"
#include <QtDebug>
Player::Player(Playlist* playlist, QObject* parent)
: QObject(parent),
playlist_(playlist),
engine_(new XineEngine)
{
if (!engine_->init()) {
qFatal("Couldn't load engine");
}
settings_.beginGroup("Player");
SetVolume(settings_.value("volume", 50).toInt());
connect(engine_, SIGNAL(stateChanged(Engine::State)), SLOT(EngineStateChanged(Engine::State)));
connect(engine_, SIGNAL(trackEnded()), SLOT(Next()));
}
void Player::Next() {
int i = playlist_->next_item();
playlist_->set_current_item(i);
if (i == -1) {
Stop();
return;
}
qDebug() << "Playing item" << i;
PlayAt(i);
}
void Player::PlayPause() {
switch (engine_->state()) {
case Engine::Paused:
qDebug() << "Unpausing";
engine_->unpause();
break;
case Engine::Playing:
qDebug() << "Pausing";
engine_->pause();
break;
case Engine::Empty:
case Engine::Idle: {
int i = playlist_->current_item();
if (i == -1) {
if (playlist_->rowCount() == 0)
break;
i = 0;
}
PlayAt(i);
break;
}
}
}
void Player::Stop() {
qDebug() << "Stopping";
engine_->stop();
playlist_->set_current_item(-1);
}
void Player::Previous() {
int i = playlist_->previous_item();
playlist_->set_current_item(i);
if (i == -1) {
Stop();
return;
}
PlayAt(i);
}
void Player::EngineStateChanged(Engine::State state) {
switch (state) {
case Engine::Paused: emit Paused(); break;
case Engine::Playing: emit Playing(); break;
case Engine::Empty:
case Engine::Idle: emit Stopped(); break;
}
}
void Player::SetVolume(int value) {
settings_.setValue("volume", value);
engine_->setVolume(value);
}
int Player::GetVolume() const {
return engine_->volume();
}
Engine::State Player::GetState() const {
return engine_->state();
}
void Player::PlayAt(int index) {
playlist_->set_current_item(index);
engine_->play(playlist_->item_at(index)->Url());
}

46
src/player.h Normal file
View File

@ -0,0 +1,46 @@
#ifndef PLAYER_H
#define PLAYER_H
#include <QObject>
#include <QSettings>
#include "engine_fwd.h"
class Playlist;
class Settings;
class Player : public QObject {
Q_OBJECT
public:
Player(Playlist* playlist, QObject* parent = 0);
EngineBase* GetEngine() { return engine_; }
Engine::State GetState() const;
int GetVolume() const;
public slots:
void PlayAt(int index);
void PlayPause();
void Next();
void Previous();
void Stop();
void SetVolume(int value);
signals:
void Playing();
void Paused();
void Stopped();
void Error(const QString& message);
private slots:
void EngineStateChanged(Engine::State);
private:
Playlist* playlist_;
QSettings settings_;
EngineBase* engine_;
};
#endif // PLAYER_H

364
src/playlist.cpp Normal file
View File

@ -0,0 +1,364 @@
#include "playlist.h"
#include "songmimedata.h"
#include "songplaylistitem.h"
#include <QtDebug>
#include <QMimeData>
#include <QBuffer>
#include <QSettings>
#include <boost/bind.hpp>
const char* Playlist::kRowsMimetype = "application/x-tangerine-playlist-rows";
const char* Playlist::kSettingsGroup = "Playlist";
Playlist::Playlist(QObject *parent) :
QAbstractListModel(parent),
current_is_paused_(false),
ignore_next_sort_(true)
{
}
Playlist::~Playlist() {
qDeleteAll(items_);
}
QVariant Playlist::headerData(int section, Qt::Orientation, int role) const {
if (role != Qt::DisplayRole)
return QVariant();
switch (section) {
case Column_Title: return "Title";
case Column_Artist: return "Artist";
case Column_Album: return "Album";
case Column_Length: return "Length";
case Column_Track: return "Track";
}
return QVariant();
}
QVariant Playlist::data(const QModelIndex& index, int role) const {
switch (role) {
case Role_IsCurrent:
return current_item_.isValid() && index.row() == current_item_.row();
case Role_IsPaused:
return current_is_paused_;
case Role_StopAfter:
return stop_after_.isValid() && stop_after_.row() == index.row();
case Qt::DisplayRole: {
PlaylistItem* item = items_[index.row()];
switch (index.column()) {
case Column_Title: return item->Title();
case Column_Artist: return item->Artist();
case Column_Album: return item->Album();
case Column_Length: return item->Length();
case Column_Track: return item->Track();
}
}
default:
return QVariant();
}
}
int Playlist::current_item() const {
return current_item_.isValid() ? current_item_.row() : -1;
}
int Playlist::next_item() const {
int i = current_item() + 1;
if (i >= items_.count())
return -1;
if (stop_after_.isValid() && current_item() == stop_after_.row())
return -1;
return i;
}
int Playlist::previous_item() const {
int i = current_item() - 1;
if (i < 0)
return -1;
return i;
}
void Playlist::set_current_item(int i) {
QModelIndex old_current = current_item_;
current_item_ = QPersistentModelIndex(index(i, 0, QModelIndex()));
if (old_current.isValid())
emit dataChanged(old_current, old_current.sibling(old_current.row(), ColumnCount));
if (current_item_.isValid())
emit dataChanged(current_item_, current_item_.sibling(current_item_.row(), ColumnCount));
}
Qt::ItemFlags Playlist::flags(const QModelIndex &index) const {
if (index.isValid())
return Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
return Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QStringList Playlist::mimeTypes() const {
return QStringList() << "text/uri-list" << kRowsMimetype;
}
Qt::DropActions Playlist::supportedDropActions() const {
return Qt::MoveAction | Qt::CopyAction;
}
bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex&) {
if (action == Qt::IgnoreAction)
return false;
QList<PlaylistItem*> added_items;
if (const SongMimeData* song_data = qobject_cast<const SongMimeData*>(data)) {
// Dragged from the library
InsertSongs(song_data->songs, row);
} else if (data->hasFormat(kRowsMimetype)) {
// Dragged from the playlist
// Rearranging it is tricky...
// Get the list of rows that were moved
QList<int> source_rows;
QDataStream stream(data->data(kRowsMimetype));
stream >> source_rows;
qStableSort(source_rows); // Make sure we take them in order
layoutAboutToBeChanged();
QList<PlaylistItem*> moved_items;
// Take the items out of the list first, keeping track of whether the
// insertion point changes
int offset = 0;
foreach (int source_row, source_rows) {
moved_items << items_.takeAt(source_row-offset);
if (row != -1 && row >= source_row)
row --;
offset++;
}
// Put the items back in
const int start = row == -1 ? items_.count() : row;
for (int i=start ; i<start+moved_items.count() ; ++i) {
items_.insert(i, moved_items[i - start]);
// Update persistent indexes
for (int column=0 ; column<ColumnCount ; ++column)
changePersistentIndex(index(source_rows[i-start], column, QModelIndex()),
index(i, column, QModelIndex()));
}
layoutChanged();
} else if (data->hasUrls()) {
// URL list dragged from some other app probably
SongList songs;
foreach (const QUrl& url, data->urls()) {
if (url.scheme() != "file")
continue;
Song song;
song.InitFromFile(url.toLocalFile(), -1);
if (!song.is_valid())
continue;
songs << songs;
}
InsertSongs(songs, row);
}
return true;
}
QModelIndex Playlist::InsertItems(const QList<PlaylistItem*>& items, int after) {
if (items.isEmpty())
return QModelIndex();
const int start = after == -1 ? items_.count() : after;
const int end = start + items.count() - 1;
beginInsertRows(QModelIndex(), start, end);
for (int i=start ; i<=end ; ++i) {
items_.insert(i, items[i - start]);
}
endInsertRows();
Save();
return index(start, 0);
}
QModelIndex Playlist::InsertSongs(const SongList& songs, int after) {
QList<PlaylistItem*> items;
foreach (const Song& song, songs) {
items << new SongPlaylistItem(song);
}
return InsertItems(items, after);
}
QMimeData* Playlist::mimeData(const QModelIndexList& indexes) const {
QMimeData* data = new QMimeData;
QList<QUrl> urls;
QList<int> rows;
foreach (const QModelIndex& index, indexes) {
if (index.column() != 0)
continue;
urls << items_[index.row()]->Url();
rows << index.row();
}
QBuffer buf;
buf.open(QIODevice::WriteOnly);
QDataStream stream(&buf);
stream << rows;
buf.close();
data->setUrls(urls);
data->setData(kRowsMimetype, buf.data());
return data;
}
bool Playlist::CompareItems(int column, Qt::SortOrder order,
const PlaylistItem* _a, const PlaylistItem* _b) {
const PlaylistItem* a = order == Qt::AscendingOrder ? _a : _b;
const PlaylistItem* b = order == Qt::AscendingOrder ? _b : _a;
switch (column) {
case Column_Title: return a->Title() < b->Title();
case Column_Artist: return a->Artist() < b->Artist();
case Column_Album: return a->Album() < b->Album();
case Column_Length: return a->Length() < b->Length();
case Column_Track: return a->Track() < b->Track();
}
return false;
}
void Playlist::sort(int column, Qt::SortOrder order) {
if (ignore_next_sort_) {
ignore_next_sort_ = false;
return;
}
layoutAboutToBeChanged();
// This is a slow and nasty way to keep the persistent indices
QMap<int, PlaylistItem*> old_persistent_mappings;
foreach (const QModelIndex& index, persistentIndexList()) {
old_persistent_mappings[index.row()] = items_[index.row()];
}
qStableSort(items_.begin(), items_.end(),
boost::bind(&Playlist::CompareItems, column, order, _1, _2));
QMapIterator<int, PlaylistItem*> it(old_persistent_mappings);
while (it.hasNext()) {
it.next();
for (int col=0 ; col<ColumnCount ; ++col) {
int new_row = items_.indexOf(it.value());
changePersistentIndex(index(it.key(), col, QModelIndex()),
index(new_row, col, QModelIndex()));
}
}
layoutChanged();
Save();
}
void Playlist::Playing() {
SetCurrentIsPaused(false);
}
void Playlist::Paused() {
SetCurrentIsPaused(true);
}
void Playlist::Stopped() {
SetCurrentIsPaused(false);
}
void Playlist::SetCurrentIsPaused(bool paused) {
if (paused == current_is_paused_)
return;
current_is_paused_ = paused;
if (current_item_.isValid())
dataChanged(index(current_item_.row(), 0),
index(current_item_.row(), ColumnCount));
}
void Playlist::Save() const {
QSettings s;
s.beginGroup(kSettingsGroup);
s.beginWriteArray("items", items_.count());
for (int i=0 ; i<items_.count() ; ++i) {
s.setArrayIndex(i);
s.setValue("type", items_.at(i)->type_string());
items_.at(i)->Save(s);
}
s.endArray();
}
void Playlist::Restore() {
qDeleteAll(items_);
items_.clear();
QSettings s;
s.beginGroup(kSettingsGroup);
int count = s.beginReadArray("items");
for (int i=0 ; i<count ; ++i) {
s.setArrayIndex(i);
QString type(s.value("type").toString());
PlaylistItem* item = PlaylistItem::NewFromType(type);
if (!item)
continue;
item->Restore(s);
items_ << item;
}
s.endArray();
reset();
}
bool Playlist::removeRows(int row, int count, const QModelIndex& parent) {
beginRemoveRows(parent, row, row+count-1);
// Remove items
for (int i=0 ; i<count ; ++i)
items_.removeAt(row);
endRemoveRows();
Save();
return true;
}
void Playlist::StopAfter(int row) {
QModelIndex old_stop_after = stop_after_;
if ((stop_after_.isValid() && stop_after_.row() == row) || row == -1)
stop_after_ = QModelIndex();
else
stop_after_ = index(row, 0);
if (old_stop_after.isValid())
emit dataChanged(old_stop_after, old_stop_after.sibling(old_stop_after.row(), ColumnCount));
if (stop_after_.isValid())
emit dataChanged(stop_after_, stop_after_.sibling(stop_after_.row(), ColumnCount));
}

87
src/playlist.h Normal file
View File

@ -0,0 +1,87 @@
#ifndef PLAYLIST_H
#define PLAYLIST_H
#include <QAbstractItemModel>
#include <QList>
#include "playlistitem.h"
#include "song.h"
class Playlist : public QAbstractListModel {
Q_OBJECT
public:
Playlist(QObject* parent = 0);
~Playlist();
enum Column {
Column_Title = 0,
Column_Artist,
Column_Album,
Column_Length,
Column_Track,
ColumnCount
};
enum Role {
Role_IsCurrent = Qt::UserRole + 1,
Role_IsPaused,
Role_StopAfter,
};
static const char* kRowsMimetype;
static const char* kSettingsGroup;
static bool CompareItems(int column, Qt::SortOrder order,
const PlaylistItem* a, const PlaylistItem* b);
// Persistence
void Save() const;
void Restore();
// Accessors
int current_item() const;
int next_item() const;
int previous_item() const;
PlaylistItem* item_at(int index) const { return items_[index]; }
// Changing the playlist
QModelIndex InsertItems(const QList<PlaylistItem*>& items, int after = -1);
QModelIndex InsertSongs(const SongList& items, int after = -1);
void StopAfter(int row);
// QAbstractListModel
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }
int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; }
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
QStringList mimeTypes() const;
Qt::DropActions supportedDropActions() const;
QMimeData* mimeData(const QModelIndexList& indexes) const;
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
void sort(int column, Qt::SortOrder order);
bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex());
public slots:
void set_current_item(int index);
void Paused();
void Playing();
void Stopped();
private:
void SetCurrentIsPaused(bool paused);
private:
QList<PlaylistItem*> items_;
QPersistentModelIndex current_item_;
QPersistentModelIndex stop_after_;
bool current_is_paused_;
// Hack to stop QTreeView::setModel sorting the playlist
bool ignore_next_sort_;
};
#endif // PLAYLIST_H

21
src/playlistitem.cpp Normal file
View File

@ -0,0 +1,21 @@
#include "playlistitem.h"
#include "songplaylistitem.h"
#include <QtDebug>
QString PlaylistItem::type_string() const {
switch (type()) {
case Type_Song: return "Song";
default:
qWarning() << "Invalid PlaylistItem type:" << type();
return QString::null;
}
}
PlaylistItem* PlaylistItem::NewFromType(const QString& type) {
if (type == "Song")
return new SongPlaylistItem;
qWarning() << "Invalid PlaylistItem type:" << type;
return NULL;
}

35
src/playlistitem.h Normal file
View File

@ -0,0 +1,35 @@
#ifndef PLAYLISTITEM_H
#define PLAYLISTITEM_H
#include <QStandardItem>
#include <QUrl>
class QSettings;
class PlaylistItem {
public:
PlaylistItem() {}
virtual ~PlaylistItem() {}
static PlaylistItem* NewFromType(const QString& type);
enum Type {
Type_Song,
};
virtual Type type() const = 0;
QString type_string() const;
virtual void Save(QSettings& settings) const = 0;
virtual void Restore(const QSettings& settings) = 0;
virtual QString Title() const = 0;
virtual QString Artist() const = 0;
virtual QString Album() const = 0;
virtual int Length() const = 0;
virtual int Track() const = 0;
virtual QUrl Url() = 0;
};
#endif // PLAYLISTITEM_H

280
src/playlistview.cpp Normal file
View File

@ -0,0 +1,280 @@
#include "playlistview.h"
#include "playlist.h"
#include <QPainter>
#include <QHeaderView>
#include <QSettings>
#include <QtDebug>
#include <QTimer>
#include <QKeyEvent>
#include <QMenu>
#include <math.h>
const char* PlaylistView::kSettingsGroup = "Playlist";
const int PlaylistView::kGlowIntensitySteps = 32;
PlaylistDelegateBase::PlaylistDelegateBase(QTreeView* view)
: QStyledItemDelegate(view),
view_(view)
{
}
void PlaylistDelegateBase::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
QStyledItemDelegate::paint(painter, Adjusted(option, index), index);
if (view_->header()->logicalIndexAt(QPoint(0,0)) == index.column()) {
if (index.data(Playlist::Role_StopAfter).toBool()) {
QColor color(Qt::white);
if (!index.data(Playlist::Role_IsCurrent).toBool() &&
!(option.state & QStyle::State_Selected)) {
color = option.palette.color(QPalette::Highlight);
}
const int kStopSize = 10;
const int kStopBorder = 2;
QRect stop_rect(option.rect);
stop_rect.setLeft(stop_rect.right() - kStopSize - kStopBorder);
stop_rect.setWidth(kStopSize);
stop_rect.moveTop(stop_rect.top() + (stop_rect.height() - kStopSize) / 2);
stop_rect.setHeight(kStopSize);
painter->setOpacity(0.65);
painter->fillRect(stop_rect, color);
painter->setOpacity(1.0);
}
}
}
QStyleOptionViewItemV4 PlaylistDelegateBase::Adjusted(const QStyleOptionViewItem& option, const QModelIndex& index) const {
if (view_->header()->logicalIndexAt(QPoint(0,0)) != index.column())
return option;
QStyleOptionViewItemV4 ret(option);
if (index.data(Playlist::Role_IsCurrent).toBool()) {
// Move the text in a bit on the first column for the song that's currently
// playing
ret.rect.setLeft(ret.rect.left() + 20);
}
return ret;
}
LengthItemDelegate::LengthItemDelegate(QTreeView* view)
: PlaylistDelegateBase(view)
{
}
QString LengthItemDelegate::displayText(const QVariant& value, const QLocale& locale) const {
bool ok = false;
int seconds = value.toInt(&ok);
QString ret = "-";
if (ok && seconds > 0) {
int hours = seconds / (60*60);
int minutes = (seconds / 60) % 60;
seconds %= 60;
if (hours)
ret.sprintf("%d:%02d:%02d", hours, minutes, seconds);
else
ret.sprintf("%d:%02d", minutes, seconds);
}
return ret;
}
PlaylistView::PlaylistView(QWidget *parent)
: QTreeView(parent),
glow_enabled_(false),
glow_timer_(new QTimer(this)),
glow_intensity_step_(0),
row_height_(-1),
currenttrack_play_(":currenttrack_play.png"),
currenttrack_pause_(":currenttrack_pause.png"),
menu_(new QMenu(this))
{
setItemDelegate(new PlaylistDelegateBase(this));
setItemDelegateForColumn(Playlist::Column_Length, new LengthItemDelegate(this));
header()->setMovable(true);
connect(header(), SIGNAL(sectionResized(int,int,int)), SLOT(SaveGeometry()));
connect(header(), SIGNAL(sectionMoved(int,int,int)), SLOT(SaveGeometry()));
glow_timer_->setInterval(1500 / kGlowIntensitySteps);
connect(glow_timer_, SIGNAL(timeout()), SLOT(GlowIntensityChanged()));
menu_->addAction(QIcon(":media-playback-stop.png"), "Stop playing after track",
this, SLOT(StopAfter()));
}
void PlaylistView::setModel(QAbstractItemModel *model) {
QTreeView::setModel(model);
LoadGeometry();
}
void PlaylistView::LoadGeometry() {
QSettings settings;
settings.beginGroup(kSettingsGroup);
header()->restoreState(settings.value("state").toByteArray());
}
void PlaylistView::SaveGeometry() {
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.setValue("state", header()->saveState());
}
void PlaylistView::ReloadBarPixmaps() {
currenttrack_bar_left_ = LoadBarPixmap(":currenttrack_bar_left.png");
currenttrack_bar_mid_ = LoadBarPixmap(":currenttrack_bar_mid.png");
currenttrack_bar_right_ = LoadBarPixmap(":currenttrack_bar_right.png");
}
QList<QPixmap> PlaylistView::LoadBarPixmap(const QString& filename) {
QImage image(filename);
image = image.scaledToHeight(row_height_, Qt::SmoothTransformation);
// Colour the bar with the palette colour
QPainter p(&image);
p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
p.setOpacity(0.4);
p.fillRect(image.rect(), palette().color(QPalette::Highlight).lighter(125));
p.end();
// Animation steps
QList<QPixmap> ret;
for(int i=0 ; i<kGlowIntensitySteps ; ++i) {
QImage step(image.copy());
p.begin(&step);
p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
p.setOpacity(0.4 - 0.6 * sin(float(i)/kGlowIntensitySteps * (M_PI/2)));
p.fillRect(step.rect(), Qt::white);
p.end();
ret << QPixmap::fromImage(step);
}
return ret;
}
void PlaylistView::drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
QStyleOptionViewItemV4 opt(option);
bool is_current = index.data(Playlist::Role_IsCurrent).toBool();
bool is_paused = index.data(Playlist::Role_IsPaused).toBool();
if (is_current) {
const_cast<PlaylistView*>(this)->last_current_item_ = index;
const_cast<PlaylistView*>(this)->last_glow_rect_ = opt.rect;
int step = glow_intensity_step_;
if (step >= kGlowIntensitySteps)
step = 2*(kGlowIntensitySteps-1) - step + 1;
int row_height = opt.rect.height();
if (row_height != row_height_) {
// Recreate the pixmaps if the height changed since last time
const_cast<PlaylistView*>(this)->row_height_ = row_height;
const_cast<PlaylistView*>(this)->ReloadBarPixmaps();
}
QRect middle(opt.rect);
middle.setLeft(middle.left() + currenttrack_bar_left_[0].width());
middle.setRight(middle.right() - currenttrack_bar_right_[0].width());
// Selection
if (selectionModel()->isSelected(index))
painter->fillRect(opt.rect, opt.palette.color(QPalette::Highlight));
// Draw the bar
painter->drawPixmap(opt.rect.topLeft(), currenttrack_bar_left_[step]);
painter->drawPixmap(opt.rect.topRight() - currenttrack_bar_right_[0].rect().topRight(), currenttrack_bar_right_[step]);
painter->drawPixmap(middle, currenttrack_bar_mid_[step]);
// Draw the play icon
QPoint play_pos(currenttrack_bar_left_[0].width() / 3 * 2,
(row_height - currenttrack_play_.height()) / 2);
painter->drawPixmap(opt.rect.topLeft() + play_pos,
is_paused ? currenttrack_pause_ : currenttrack_play_);
// Set the font
opt.palette.setColor(QPalette::Text, Qt::white);
opt.palette.setColor(QPalette::HighlightedText, Qt::white);
opt.palette.setColor(QPalette::Highlight, Qt::transparent);
opt.font.setItalic(true);
opt.decorationSize = QSize(20,20);
}
QTreeView::drawRow(painter, opt, index);
}
void PlaylistView::GlowIntensityChanged() {
glow_intensity_step_ = (glow_intensity_step_ + 1) % (kGlowIntensitySteps * 2);
viewport()->update(last_glow_rect_);
}
void PlaylistView::StopGlowing() {
glow_enabled_ = false;
glow_timer_->stop();
glow_intensity_step_ = kGlowIntensitySteps;
}
void PlaylistView::StartGlowing() {
glow_enabled_ = true;
if (isVisible())
glow_timer_->start();
}
void PlaylistView::hideEvent(QHideEvent*) {
glow_timer_->stop();
}
void PlaylistView::showEvent(QShowEvent*) {
if (glow_enabled_)
glow_timer_->start();
}
bool CompareSelectionRanges(const QItemSelectionRange& a, const QItemSelectionRange& b) {
return b.bottom() < a.bottom();
}
void PlaylistView::keyPressEvent(QKeyEvent* event) {
if (model() && event->matches(QKeySequence::Delete)) {
QItemSelection selection(selectionModel()->selection());
// Sort the selection so we remove the items at the *bottom* first, ensuring
// we don't have to mess around with changing row numbers
qSort(selection.begin(), selection.end(), CompareSelectionRanges);
foreach (const QItemSelectionRange& range, selection) {
model()->removeRows(range.top(), range.height(), range.parent());
}
event->accept();
} else {
QTreeView::keyPressEvent(event);
}
}
void PlaylistView::contextMenuEvent(QContextMenuEvent* e) {
menu_index_ = indexAt(e->pos());
if (!menu_index_.isValid())
return;
menu_->popup(e->globalPos());
e->accept();
}
void PlaylistView::StopAfter() {
Playlist* playlist = qobject_cast<Playlist*>(model());
Q_ASSERT(playlist);
playlist->StopAfter(menu_index_.row());
}

79
src/playlistview.h Normal file
View File

@ -0,0 +1,79 @@
#ifndef PLAYLISTVIEW_H
#define PLAYLISTVIEW_H
#include <QStyledItemDelegate>
#include <QTreeView>
class PlaylistDelegateBase : public QStyledItemDelegate {
public:
PlaylistDelegateBase(QTreeView* view);
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
QStyleOptionViewItemV4 Adjusted(const QStyleOptionViewItem& option, const QModelIndex& index) const;
protected:
QTreeView* view_;
};
class LengthItemDelegate : public PlaylistDelegateBase {
public:
LengthItemDelegate(QTreeView* view);
QString displayText(const QVariant& value, const QLocale& locale) const;
};
class PlaylistView : public QTreeView {
Q_OBJECT
public:
PlaylistView(QWidget* parent = 0);
// QTreeView
void setModel(QAbstractItemModel *model);
void drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
void keyPressEvent(QKeyEvent* event);
// QAbstractScrollArea
void contextMenuEvent(QContextMenuEvent* e);
public slots:
void StopGlowing();
void StartGlowing();
protected:
void hideEvent(QHideEvent* event);
void showEvent(QShowEvent* event);
private slots:
void LoadGeometry();
void SaveGeometry();
void GlowIntensityChanged();
void StopAfter();
private:
void ReloadBarPixmaps();
QList<QPixmap> LoadBarPixmap(const QString& filename);
private:
static const char* kSettingsGroup;
static const int kGlowIntensitySteps;
bool glow_enabled_;
QTimer* glow_timer_;
int glow_intensity_step_;
QModelIndex last_current_item_;
QRect last_glow_rect_;
int row_height_; // Used to invalidate the currenttrack_bar pixmaps
QList<QPixmap> currenttrack_bar_left_;
QList<QPixmap> currenttrack_bar_mid_;
QList<QPixmap> currenttrack_bar_right_;
QPixmap currenttrack_play_;
QPixmap currenttrack_pause_;
QMenu* menu_;
QModelIndex menu_index_;
};
#endif // PLAYLISTVIEW_H

369
src/sliderwidget.cpp Normal file
View File

@ -0,0 +1,369 @@
/***************************************************************************
amarokslider.cpp - description
-------------------
begin : Dec 15 2003
copyright : (C) 2003 by Mark Kretschmann
email : markey@web.de
copyright : (C) 2005 by Gábor Lehel
email : illissius@gmail.com
***************************************************************************/
/***************************************************************************
* *
* 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 "sliderwidget.h"
#include <QApplication>
#include <QBitmap>
#include <QBrush>
#include <QImage>
#include <QPainter>
#include <QSize>
#include <QTimer>
#include <QStyle>
#include <QMenu>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QStyleOptionSlider>
Amarok::Slider::Slider( Qt::Orientation orientation, QWidget *parent, uint max )
: QSlider( orientation, parent )
, m_sliding( false )
, m_outside( false )
, m_prevValue( 0 )
{
setRange( 0, max );
}
void
Amarok::Slider::wheelEvent( QWheelEvent *e )
{
if( orientation() == Qt::Vertical ) {
// Will be handled by the parent widget
e->ignore();
return;
}
// Position Slider (horizontal)
int step = e->delta() * 1500 / 18;
int nval = QSlider::value() + step;
nval = qMax(nval, minimum());
nval = qMin(nval, maximum());
QSlider::setValue( nval );
emit sliderReleased( value() );
}
void
Amarok::Slider::mouseMoveEvent( QMouseEvent *e )
{
if ( m_sliding )
{
//feels better, but using set value of 20 is bad of course
QRect rect( -20, -20, width()+40, height()+40 );
if ( orientation() == Qt::Horizontal && !rect.contains( e->pos() ) ) {
if ( !m_outside )
QSlider::setValue( m_prevValue );
m_outside = true;
} else {
m_outside = false;
slideEvent( e );
emit sliderMoved( value() );
}
}
else QSlider::mouseMoveEvent( e );
}
void
Amarok::Slider::slideEvent( QMouseEvent *e )
{
QStyleOptionSlider option;
initStyleOption(&option);
QRect sliderRect(style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, this));
QSlider::setValue( orientation() == Qt::Horizontal
? ((QApplication::layoutDirection() == Qt::RightToLeft) ?
QStyle::sliderValueFromPosition(minimum(), maximum(), width() - (e->pos().x() - sliderRect.width()/2), width() + sliderRect.width(), true )
: QStyle::sliderValueFromPosition(minimum(), maximum(), e->pos().x() - sliderRect.width()/2, width() - sliderRect.width() ) )
: QStyle::sliderValueFromPosition(minimum(), maximum(), e->pos().y() - sliderRect.height()/2, height() - sliderRect.height() ) );
}
void
Amarok::Slider::mousePressEvent( QMouseEvent *e )
{
QStyleOptionSlider option;
initStyleOption(&option);
QRect sliderRect(style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, this));
m_sliding = true;
m_prevValue = QSlider::value();
if ( !sliderRect.contains( e->pos() ) )
mouseMoveEvent( e );
}
void
Amarok::Slider::mouseReleaseEvent( QMouseEvent* )
{
if( !m_outside && QSlider::value() != m_prevValue )
emit sliderReleased( value() );
m_sliding = false;
m_outside = false;
}
void
Amarok::Slider::setValue( int newValue )
{
//don't adjust the slider while the user is dragging it!
if ( !m_sliding || m_outside )
QSlider::setValue( adjustValue( newValue ) );
else
m_prevValue = newValue;
}
//////////////////////////////////////////////////////////////////////////////////////////
/// CLASS PrettySlider
//////////////////////////////////////////////////////////////////////////////////////////
#define THICKNESS 7
#define MARGIN 3
Amarok::PrettySlider::PrettySlider( Qt::Orientation orientation, SliderMode mode,
QWidget *parent, uint max )
: Amarok::Slider( orientation, parent, max )
, m_mode( mode )
{
if( m_mode == Pretty)
{
setFocusPolicy( Qt::NoFocus );
}
}
void
Amarok::PrettySlider::mousePressEvent( QMouseEvent *e )
{
Amarok::Slider::mousePressEvent( e );
slideEvent( e );
}
void
Amarok::PrettySlider::slideEvent( QMouseEvent *e )
{
if( m_mode == Pretty )
QSlider::setValue( orientation() == Qt::Horizontal
? QStyle::sliderValueFromPosition(minimum(), maximum(), e->pos().x(), width()-2 )
: QStyle::sliderValueFromPosition(minimum(), maximum(), e->pos().y(), height()-2 ) );
else
Amarok::Slider::slideEvent( e );
}
namespace Amarok {
namespace ColorScheme {
extern QColor Background;
extern QColor Foreground;
}
}
#if 0
/** these functions aren't required in our fixed size world,
but they may become useful one day **/
QSize
Amarok::PrettySlider::minimumSizeHint() const
{
return sizeHint();
}
QSize
Amarok::PrettySlider::sizeHint() const
{
constPolish();
return (orientation() == Horizontal
? QSize( maxValue(), THICKNESS + MARGIN )
: QSize( THICKNESS + MARGIN, maxValue() )).expandedTo( QApplit ication::globalStrut() );
}
#endif
//////////////////////////////////////////////////////////////////////////////////////////
/// CLASS VolumeSlider
//////////////////////////////////////////////////////////////////////////////////////////
Amarok::VolumeSlider::VolumeSlider( QWidget *parent, uint max )
: Amarok::Slider( Qt::Horizontal, parent, max )
, m_animCount( 0 )
, m_animTimer( new QTimer( this ) )
, m_pixmapInset( QPixmap( ":volumeslider-inset.png" ))
{
setFocusPolicy( Qt::NoFocus );
// BEGIN Calculate handle animation pixmaps for mouse-over effect
QImage pixmapHandle ( ":volumeslider-handle.png" );
QImage pixmapHandleGlow( ":volumeslider-handle_glow.png" );
float opacity = 0.0;
const float step = 1.0 / ANIM_MAX;
QImage dst;
for ( int i = 0; i < ANIM_MAX; ++i ) {
dst = pixmapHandle.copy();
QPainter p(&dst);
p.setOpacity(opacity);
p.drawImage(0, 0, pixmapHandleGlow);
p.end();
m_handlePixmaps.append( QPixmap::fromImage( dst ) );
opacity += step;
}
// END
generateGradient();
setMinimumWidth( m_pixmapInset.width() );
setMinimumHeight( m_pixmapInset.height() );
connect( m_animTimer, SIGNAL( timeout() ), this, SLOT( slotAnimTimer() ) );
}
void
Amarok::VolumeSlider::generateGradient()
{
const QImage mask( ":volumeslider-gradient.png" );
QImage gradient_image(mask.size(), QImage::Format_ARGB32_Premultiplied);
QPainter p(&gradient_image);
QLinearGradient gradient(gradient_image.rect().topLeft(),
gradient_image.rect().topRight());
gradient.setColorAt(0, palette().color(QPalette::Background));
gradient.setColorAt(1, palette().color(QPalette::Highlight));
p.fillRect(gradient_image.rect(), QBrush(gradient));
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(0, 0, mask);
p.end();
m_pixmapGradient = QPixmap::fromImage(gradient_image);
}
void
Amarok::VolumeSlider::slotAnimTimer() //SLOT
{
if ( m_animEnter ) {
m_animCount++;
update();
if ( m_animCount == ANIM_MAX - 1 )
m_animTimer->stop();
} else {
m_animCount--;
update();
if ( m_animCount == 0 )
m_animTimer->stop();
}
}
void
Amarok::VolumeSlider::mousePressEvent( QMouseEvent *e )
{
if( e->button() != Qt::RightButton ) {
Amarok::Slider::mousePressEvent( e );
slideEvent( e );
}
}
void
Amarok::VolumeSlider::contextMenuEvent( QContextMenuEvent *e )
{
QMap<QAction*, int> values;
QMenu menu;
menu.setTitle("Volume");
values[menu.addAction("100%" )] = 100;
values[menu.addAction("80%" )] = 80;
values[menu.addAction("60%" )] = 60;
values[menu.addAction("40%" )] = 40;
values[menu.addAction("20%" )] = 20;
values[menu.addAction("0%" )] = 0;
QAction* ret = menu.exec( mapToGlobal( e->pos() ) );
if( ret )
{
QSlider::setValue( values[ret] );
emit sliderReleased( values[ret] );
}
}
void
Amarok::VolumeSlider::slideEvent( QMouseEvent *e )
{
QSlider::setValue( QStyle::sliderValueFromPosition(minimum(), maximum(), e->pos().x(), width()-2 ) );
}
void
Amarok::VolumeSlider::wheelEvent( QWheelEvent *e )
{
const uint step = e->delta() / 10;
QSlider::setValue( QSlider::value() + step );
emit sliderReleased( value() );
}
void
Amarok::VolumeSlider::paintEvent( QPaintEvent * )
{
QPainter p(this);
const int padding = 7;
const int offset = int( double( ( width() - 2 * padding ) * value() ) / maximum() );
p.drawPixmap(0, 0, m_pixmapGradient, 0, 0, offset + padding, 0);
p.drawPixmap(0, 0, m_pixmapInset);
p.drawPixmap(offset - m_handlePixmaps[0].width() / 2 + padding, 0, m_handlePixmaps[m_animCount]);
// Draw percentage number
p.setPen( palette().color( QPalette::Disabled, QPalette::Text ).dark() );
QFont font;
font.setPixelSize( 9 );
p.setFont( font );
const QRect rect( 0, 0, 34, 15 );
p.drawText( rect, Qt::AlignRight | Qt::AlignVCenter, QString::number( value() ) + '%' );
}
void
Amarok::VolumeSlider::enterEvent( QEvent* )
{
m_animEnter = true;
m_animCount = 0;
m_animTimer->start( ANIM_INTERVAL );
}
void
Amarok::VolumeSlider::leaveEvent( QEvent* )
{
// This can happen if you enter and leave the widget quickly
if ( m_animCount == 0 )
m_animCount = 1;
m_animEnter = false;
m_animTimer->start( ANIM_INTERVAL );
}
void
Amarok::VolumeSlider::paletteChange( const QPalette& )
{
generateGradient();
}

145
src/sliderwidget.h Normal file
View File

@ -0,0 +1,145 @@
/***************************************************************************
amarokslider.h - description
-------------------
begin : Dec 15 2003
copyright : (C) 2003 by Mark Kretschmann
email : markey@web.de
copyright : (C) 2005 by Gábor Lehel
email : illissius@gmail.com
***************************************************************************/
/***************************************************************************
* *
* 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. *
* *
***************************************************************************/
#ifndef AMAROKSLIDER_H
#define AMAROKSLIDER_H
#include <QPixmap>
#include <QSlider>
#include <QList>
class QPalette;
class QTimer;
namespace Amarok
{
class Slider : public QSlider
{
Q_OBJECT
public:
Slider( Qt::Orientation, QWidget*, uint max = 0 );
virtual void setValue( int );
//WARNING non-virtual - and thus only really intended for internal use
//this is a major flaw in the class presently, however it suits our
//current needs fine
int value() const { return adjustValue( QSlider::value() ); }
signals:
//we emit this when the user has specifically changed the slider
//so connect to it if valueChanged() is too generic
//Qt also emits valueChanged( int )
void sliderReleased( int );
protected:
virtual void wheelEvent( QWheelEvent* );
virtual void mouseMoveEvent( QMouseEvent* );
virtual void mouseReleaseEvent( QMouseEvent* );
virtual void mousePressEvent( QMouseEvent* );
virtual void slideEvent( QMouseEvent* );
bool m_sliding;
/// we flip the value for vertical sliders
int adjustValue( int v ) const
{
int mp = (minimum() + maximum()) / 2;
return orientation() == Qt::Vertical ? mp - (v - mp) : v;
}
private:
bool m_outside;
int m_prevValue;
Slider( const Slider& ); //undefined
Slider &operator=( const Slider& ); //undefined
};
class PrettySlider : public Slider
{
Q_OBJECT
public:
typedef enum
{
Normal, // Same behavior as Slider *unless* there's a moodbar
Pretty
} SliderMode;
PrettySlider( Qt::Orientation orientation, SliderMode mode,
QWidget *parent, uint max = 0 );
protected:
virtual void slideEvent( QMouseEvent* );
virtual void mousePressEvent( QMouseEvent* );
private:
PrettySlider( const PrettySlider& ); //undefined
PrettySlider &operator=( const PrettySlider& ); //undefined
SliderMode m_mode;
};
class VolumeSlider: public Slider
{
Q_OBJECT
public:
VolumeSlider( QWidget *parent, uint max = 0 );
protected:
virtual void paintEvent( QPaintEvent* );
virtual void enterEvent( QEvent* );
virtual void leaveEvent( QEvent* );
virtual void paletteChange( const QPalette& );
virtual void slideEvent( QMouseEvent* );
virtual void mousePressEvent( QMouseEvent* );
virtual void contextMenuEvent( QContextMenuEvent* );
virtual void wheelEvent( QWheelEvent *e );
private slots:
virtual void slotAnimTimer();
private:
void generateGradient();
VolumeSlider( const VolumeSlider& ); //undefined
VolumeSlider &operator=( const VolumeSlider& ); //undefined
////////////////////////////////////////////////////////////////
static const int ANIM_INTERVAL = 18;
static const int ANIM_MAX = 18;
bool m_animEnter;
int m_animCount;
QTimer* m_animTimer;
QPixmap m_pixmapInset;
QPixmap m_pixmapGradient;
QList<QPixmap> m_handlePixmaps;
};
}
#endif

278
src/song.cpp Normal file
View File

@ -0,0 +1,278 @@
#include "song.h"
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <taglib/tstring.h>
#include <taglib/mpegfile.h>
#include <taglib/id3v2tag.h>
#include <taglib/oggfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/flacfile.h>
#include <QFile>
#include <QFileInfo>
#include <QTime>
#include <QSqlQuery>
#include <QVariant>
const char* Song::kColumnSpec =
"title, album, artist, albumartist, composer, "
"track, disc, bpm, year, genre, comment, compilation, "
"length, bitrate, samplerate, directory, filename, "
"mtime, ctime, filesize";
const char* Song::kBindSpec =
":title, :album, :artist, :albumartist, :composer, "
":track, :disc, :bpm, :year, :genre, :comment, :compilation, "
":length, :bitrate, :samplerate, :directory_id, :filename, "
":mtime, :ctime, :filesize";
const char* Song::kUpdateSpec =
"title = :title, album = :album, artist = :artist, "
"albumartist = :albumartist, composer = :composer, track = :track, "
"disc = :disc, bpm = :bpm, year = :year, genre = :genre, "
"comment = :comment, compilation = :compilation, length = :length, "
"bitrate = :bitrate, samplerate = :samplerate, "
"directory = :directory_id, filename = :filename, mtime = :mtime, "
"ctime = :ctime, filesize = :filesize";
Song::Song()
: valid_(false),
id_(-1),
track_(-1),
disc_(-1),
bpm_(-1),
year_(-1),
compilation_(false),
length_(-1),
bitrate_(-1),
samplerate_(-1),
directory_id_(-1),
mtime_(-1),
ctime_(-1),
filesize_(-1)
{
}
void Song::InitFromFile(const QString& filename, int directory_id) {
filename_ = filename;
directory_id_ = directory_id;
TagLib::FileRef fileref = TagLib::FileRef(QFile::encodeName(filename).constData());
if( fileref.isNull() )
return;
QFileInfo info(filename);
filesize_ = info.size();
mtime_ = info.lastModified().toTime_t();
ctime_ = info.created().toTime_t();
TagLib::Tag* tag = fileref.tag();
if (tag) {
#define strip(x) TStringToQString( x ).trimmed()
title_ = strip(tag->title());
artist_ = strip(tag->artist());
album_ = strip(tag->album());
comment_ = strip(tag->comment());
genre_ = strip(tag->genre());
year_ = tag->year();
track_ = tag->track();
#undef strip
valid_ = true;
}
QString disc;
QString compilation;
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref.file())) {
if (file->ID3v2Tag()) {
if (!file->ID3v2Tag()->frameListMap()["TPOS"].isEmpty())
disc = TStringToQString(file->ID3v2Tag()->frameListMap()["TPOS"].front()->toString()).trimmed();
if (!file->ID3v2Tag()->frameListMap()["TBPM"].isEmpty())
bpm_ = TStringToQString(file->ID3v2Tag()->frameListMap()["TBPM"].front()->toString()).trimmed().toFloat();
if (!file->ID3v2Tag()->frameListMap()["TCOM"].isEmpty())
composer_ = TStringToQString(file->ID3v2Tag()->frameListMap()["TCOM"].front()->toString()).trimmed();
if (!file->ID3v2Tag()->frameListMap()["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
albumartist_ = TStringToQString(file->ID3v2Tag()->frameListMap()["TPE2"].front()->toString()).trimmed();
if (!file->ID3v2Tag()->frameListMap()["TCMP"].isEmpty())
compilation = TStringToQString(file->ID3v2Tag()->frameListMap()["TCMP"].front()->toString()).trimmed();
}
}
else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref.file())) {
if (file->tag()) {
if ( !file->tag()->fieldListMap()["COMPOSER"].isEmpty() )
composer_ = TStringToQString(file->tag()->fieldListMap()["COMPOSER"].front()).trimmed();
if ( !file->tag()->fieldListMap()["BPM"].isEmpty() )
bpm_ = TStringToQString(file->tag()->fieldListMap()["BPM"].front()).trimmed().toFloat();
if ( !file->tag()->fieldListMap()["DISCNUMBER"].isEmpty() )
disc = TStringToQString(file->tag()->fieldListMap()["DISCNUMBER"].front()).trimmed();
if ( !file->tag()->fieldListMap()["COMPILATION"].isEmpty() )
compilation = TStringToQString(file->tag()->fieldListMap()["COMPILATION"].front()).trimmed();
}
}
else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref.file())) {
if ( file->xiphComment() ) {
if (!file->xiphComment()->fieldListMap()["COMPOSER"].isEmpty())
composer_ = TStringToQString( file->xiphComment()->fieldListMap()["COMPOSER"].front() ).trimmed();
if (!file->xiphComment()->fieldListMap()["BPM"].isEmpty() )
bpm_ = TStringToQString( file->xiphComment()->fieldListMap()["BPM"].front() ).trimmed().toFloat();
if (!file->xiphComment()->fieldListMap()["DISCNUMBER"].isEmpty() )
disc = TStringToQString( file->xiphComment()->fieldListMap()["DISCNUMBER"].front() ).trimmed();
if (!file->xiphComment()->fieldListMap()["COMPILATION"].isEmpty() )
compilation = TStringToQString( file->xiphComment()->fieldListMap()["COMPILATION"].front() ).trimmed();
}
}
if ( !disc.isEmpty() ) {
int i = disc.indexOf('/');
if ( i != -1 )
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
disc_ = disc.left( i ).toInt();
else
disc_ = disc.toInt();
}
if ( compilation.isEmpty() ) {
// well, it wasn't set, but if the artist is VA assume it's a compilation
if ( artist_.toLower() == "various artists" )
compilation_ = true;
} else {
int i = compilation.toInt();
compilation_ = (i == 1);
}
bitrate_ = fileref.audioProperties()->bitrate();
length_ = fileref.audioProperties()->length();
samplerate_ = fileref.audioProperties()->sampleRate();
}
void Song::InitFromQuery(const QSqlQuery& q) {
valid_ = true;
#define tostr(n) (q.value(n).isNull() ? QString::null : q.value(n).toString())
#define toint(n) (q.value(n).isNull() ? -1 : q.value(n).toInt())
#define tofloat(n) (q.value(n).isNull() ? -1 : q.value(n).toDouble())
id_ = toint(0);
title_ = tostr(1);
album_ = tostr(2);
artist_ = tostr(3);
albumartist_ = tostr(4);
composer_ = tostr(5);
track_ = toint(6);
disc_ = toint(7);
bpm_ = tofloat(8);
year_ = toint(9);
genre_ = tostr(10);
comment_ = tostr(11);
compilation_ = q.value(12).toBool();
length_ = toint(13);
bitrate_ = toint(14);
samplerate_ = toint(15);
directory_id_ = toint(16);
filename_ = tostr(17);
mtime_ = toint(18);
ctime_ = toint(19);
filesize_ = toint(20);
#undef tostr
#undef toint
#undef tofloat
}
void Song::BindToQuery(QSqlQuery *query) const {
#define intval(x) (x == -1 ? QVariant() : x)
query->bindValue(":title", title_);
query->bindValue(":album", album_);
query->bindValue(":artist", artist_);
query->bindValue(":albumartist", albumartist_);
query->bindValue(":composer", composer_);
query->bindValue(":track", intval(track_));
query->bindValue(":disc", intval(disc_));
query->bindValue(":bpm", intval(bpm_));
query->bindValue(":year", intval(year_));
query->bindValue(":genre", genre_);
query->bindValue(":comment", comment_);
query->bindValue(":compilation", compilation_ ? 1 : 0);
query->bindValue(":length", intval(length_));
query->bindValue(":bitrate", intval(bitrate_));
query->bindValue(":samplerate", intval(samplerate_));
query->bindValue(":directory_id", intval(directory_id_));
query->bindValue(":filename", filename_);
query->bindValue(":mtime", intval(mtime_));
query->bindValue(":ctime", intval(ctime_));
query->bindValue(":filesize", intval(filesize_));
#undef intval
}
QString Song::PrettyTitleWithArtist() const {
QString title(title_);
if (title.isEmpty())
title = QFileInfo(filename_).baseName();
if (compilation_ && !artist_.isEmpty())
title = artist_ + " - " + title;
return title;
}
QString Song::PrettyTitle() const {
QString title(title_);
if (title.isEmpty())
title = QFileInfo(filename_).baseName();
return title;
}
QString Song::PrettyLength() const {
if (length_ == -1)
return QString::null;
int hours = length_ / (60*60);
int minutes = (length_ / 60) % 60;
int seconds = length_ % 60;
QString text;
if (hours)
text.sprintf("%d:%02d:%02d", hours, minutes, seconds);
else
text.sprintf("%d:%02d", minutes, seconds);
return text;
}
bool Song::IsMetadataEqual(const Song& other) const {
return title_ == other.title_ &&
album_ == other.album_ &&
artist_ == other.artist_ &&
albumartist_ == other.albumartist_ &&
composer_ == other.composer_ &&
track_ == other.track_ &&
disc_ == other.disc_ &&
qFuzzyCompare(bpm_, other.bpm_) &&
year_ == other.year_ &&
genre_ == other.genre_ &&
comment_ == other.comment_ &&
compilation_ == other.compilation_ &&
length_ == other.length_ &&
bitrate_ == other.bitrate_ &&
samplerate_ == other.samplerate_;
}

91
src/song.h Normal file
View File

@ -0,0 +1,91 @@
#ifndef SONG_H
#define SONG_H
#include <QString>
#include <QList>
#include <QSqlQuery>
class Song {
public:
Song();
static const char* kColumnSpec;
static const char* kBindSpec;
static const char* kUpdateSpec;
// Constructors
void InitFromFile(const QString& filename, int directory_id);
void InitFromQuery(const QSqlQuery& query);
// Save
void BindToQuery(QSqlQuery* query) const;
// Simple accessors
bool is_valid() const { return valid_; }
int id() const { return id_; }
QString title() const { return title_; }
QString album() const { return album_; }
QString artist() const { return artist_; }
QString albumartist() const { return albumartist_; }
QString composer() const { return composer_; }
int track() const { return track_; }
int disc() const { return disc_; }
float bpm() const { return bpm_; }
int year() const { return year_; }
QString genre() const { return genre_; }
QString comment() const { return comment_; }
bool is_compilation() const { return compilation_; }
int length() const { return length_; }
int bitrate() const { return bitrate_; }
int samplerate() const { return samplerate_; }
int directory_id() const { return directory_id_; }
QString filename() const { return filename_; }
uint mtime() const { return mtime_; }
uint ctime() const { return ctime_; }
int filesize() const { return filesize_; }
// Pretty accessors
QString PrettyTitle() const;
QString PrettyTitleWithArtist() const;
QString PrettyLength() const;
// Setters
void set_id(int id) { id_ = id; }
// Comparison functions
bool IsMetadataEqual(const Song& other) const;
private:
bool valid_;
int id_;
QString title_;
QString album_;
QString artist_;
QString albumartist_;
QString composer_;
int track_;
int disc_;
float bpm_;
int year_;
QString genre_;
QString comment_;
bool compilation_;
int length_;
int bitrate_;
int samplerate_;
int directory_id_;
QString filename_;
int mtime_;
int ctime_;
int filesize_;
};
typedef QList<Song> SongList;
#endif // SONG_H

1
src/songmimedata.cpp Normal file
View File

@ -0,0 +1 @@
#include "songmimedata.h"

15
src/songmimedata.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef SONGMIMEDATA_H
#define SONGMIMEDATA_H
#include <QMimeData>
#include "song.h"
class SongMimeData : public QMimeData {
Q_OBJECT
public:
SongList songs;
};
#endif // SONGMIMEDATA_H

51
src/songplaylistitem.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "songplaylistitem.h"
#include <QtDebug>
#include <QSettings>
SongPlaylistItem::SongPlaylistItem()
{
}
SongPlaylistItem::SongPlaylistItem(const Song& song)
: song_(song)
{
}
void SongPlaylistItem::Save(QSettings& settings) const {
settings.setValue("filename", song_.filename());
settings.setValue("library_directory", song_.directory_id());
}
void SongPlaylistItem::Restore(const QSettings& settings) {
QString filename(settings.value("filename").toString());
int directory_id(settings.value("library_directory", -1).toInt());
song_.InitFromFile(filename, directory_id);
}
QString SongPlaylistItem::Title() const {
return song_.PrettyTitle();
}
QString SongPlaylistItem::Artist() const {
return song_.artist();
}
QString SongPlaylistItem::Album() const {
return song_.album();
}
int SongPlaylistItem::Length() const {
return song_.length();
}
int SongPlaylistItem::Track() const {
return song_.track();
}
QUrl SongPlaylistItem::Url() {
QUrl ret(QUrl::fromLocalFile(song_.filename()));
ret.setHost("localhost");
return ret;
}

29
src/songplaylistitem.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef SONGPLAYLISTITEM_H
#define SONGPLAYLISTITEM_H
#include "playlistitem.h"
#include "song.h"
class SongPlaylistItem : public PlaylistItem {
public:
SongPlaylistItem();
SongPlaylistItem(const Song& song);
Type type() const { return Type_Song; }
void Save(QSettings& settings) const;
void Restore(const QSettings& settings);
QString Title() const;
QString Artist() const;
QString Album() const;
int Length() const;
int Track() const;
QUrl Url();
private:
Song song_;
};
#endif // SONGPLAYLISTITEM_H

80
src/src.pro Normal file
View File

@ -0,0 +1,80 @@
# -------------------------------------------------
# Project created by QtCreator 2009-12-15T18:38:35
# -------------------------------------------------
QT += sql \
opengl
TARGET = tangerine
TEMPLATE = app
SOURCES += main.cpp \
mainwindow.cpp \
player.cpp \
library.cpp \
librarybackend.cpp \
playlist.cpp \
playlistitem.cpp \
enginebase.cpp \
analyzers/baranalyzer.cpp \
analyzers/analyzerbase.cpp \
fht.cpp \
analyzers/blockanalyzer.cpp \
xine-engine.cpp \
xine-scope.c \
sliderwidget.cpp \
playlistview.cpp \
backgroundthread.cpp \
librarywatcher.cpp \
song.cpp \
songmimedata.cpp \
songplaylistitem.cpp \
libraryview.cpp \
libraryitem.cpp \
libraryconfig.cpp \
systemtrayicon.cpp \
libraryquery.cpp
HEADERS += mainwindow.h \
player.h \
library.h \
librarybackend.h \
playlist.h \
playlistitem.h \
enginebase.h \
engine_fwd.h \
analyzers/baranalyzer.h \
analyzers/analyzerbase.h \
fht.h \
analyzers/blockanalyzer.h \
xine-engine.h \
xine-scope.h \
sliderwidget.h \
playlistview.h \
backgroundthread.h \
librarywatcher.h \
directory.h \
song.h \
songmimedata.h \
songplaylistitem.h \
libraryview.h \
libraryitem.h \
libraryconfig.h \
systemtrayicon.h \
libraryquery.h
FORMS += mainwindow.ui \
libraryconfig.ui
RESOURCES += ../data/data.qrc
OTHER_FILES += ../data/schema.sql \
../data/mainwindow.css
!win32 {
QMAKE_CXXFLAGS += $$system(taglib-config --cflags)
LIBS += $$system(taglib-config --libs)
QMAKE_CXXFLAGS += $$system(xine-config --cflags)
LIBS += $$system(xine-config --libs)
}
win32 {
INCLUDEPATH += C:/msys/1.0/local/include \
C:/msys/1.0/local/include/taglib
LIBS += -Lc:/msys/1.0/local/lib \
-Lc:/msys/1.0/local/bin \
-ltag \
-lxine \
-lpthreadGC2
}

17
src/systemtrayicon.cpp Normal file
View File

@ -0,0 +1,17 @@
#include "systemtrayicon.h"
#include <QEvent>
#include <QWheelEvent>
SystemTrayIcon::SystemTrayIcon(QObject* parent)
: QSystemTrayIcon(parent)
{
}
bool SystemTrayIcon::event(QEvent* event) {
if (event->type() == QEvent::Wheel) {
emit WheelEvent(static_cast<QWheelEvent*>(event)->delta());
return true;
}
return QSystemTrayIcon::event(event);
}

18
src/systemtrayicon.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef SYSTEMTRAYICON_H
#define SYSTEMTRAYICON_H
#include <QSystemTrayIcon>
class SystemTrayIcon : public QSystemTrayIcon {
Q_OBJECT
public:
SystemTrayIcon(QObject* parent = 0);
bool event(QEvent* event);
signals:
void WheelEvent(int delta);
};
#endif // SYSTEMTRAYICON_H

1282
src/xine-engine.cpp Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More