1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-01-17 22:38:33 +01:00

Add a summary generator to the dynamic playlists. Now when you collapse the controls a sentence representing the controls is shown.

This will need some work to make the english flow better, but this is a good start.
This commit is contained in:
Leo Franchi 2011-01-27 22:22:41 -05:00
parent 7bf2eb3c9e
commit 1fd4464891
10 changed files with 218 additions and 28 deletions

View File

@ -61,6 +61,7 @@
<file>./data/images/list-add.png</file>
<file>./data/images/list-remove.png</file>
<file>./data/images/arrow-up-double.png</file>
<file>./data/images/arrow-down-double.png</file>
<file>./data/images/volume-icon-full.png</file>
<file>./data/images/volume-icon-muted.png</file>
<file>./data/images/volume-slider-bkg.png</file>

View File

@ -44,6 +44,7 @@ class DynamicControl : public QObject
Q_PROPERTY( QString selectedType READ selectedType WRITE setSelectedType )
Q_PROPERTY( QString match READ match WRITE setMatch )
Q_PROPERTY( QString input READ input WRITE setInput )
Q_PROPERTY( QString summary READ summary ) // a summary of the control in phrase form
public:
DynamicControl( const QStringList& typeSelectors = QStringList() );
@ -66,17 +67,18 @@ public:
virtual QWidget* inputField() { Q_ASSERT( false ); return 0; }
/// The user-readable match value, for showing in read-only playlists
virtual QString matchString() { Q_ASSERT( false ); return QString(); }
virtual QString matchString() const { Q_ASSERT( false ); return QString(); }
/// the serializable value of the match
virtual QString match() const { Q_ASSERT( false ); return QString(); }
/// the serializable value of the input
virtual QString input() const { Q_ASSERT( false ); return QString(); }
/// the user-readable summary phrase
virtual QString summary() const { Q_ASSERT( false ); return QString(); }
// used by JSON serialization
virtual void setMatch( const QString& match ) { Q_ASSERT( false ); }
virtual void setInput( const QString& input ) { Q_ASSERT( false ); }
/// All the potential type selectors for this control
QStringList typeSelectors() const { return m_typeSelectors; }
@ -104,9 +106,6 @@ protected:
// Private constructor, you can't make one. Get it from your Generator.
explicit DynamicControl( const QString& selectedType, const QStringList& typeSelectors, QObject* parent = 0 );
QString m_match;
QString m_input;
private:
QString m_type;
QString m_selectedType;

View File

@ -80,6 +80,11 @@ public:
*/
virtual void fetchNext( int rating = -1 ) {}
/**
* Return a sentence that describes this generator's controls. TODO english only ATM
*/
virtual QString sentenceSummary() {}
/// The type of this generator
QString type() const { return m_type; }

View File

@ -76,30 +76,40 @@ Tomahawk::EchonestControl::toENParam() const
return m_data;
}
QString Tomahawk::EchonestControl::input() const
QString
Tomahawk::EchonestControl::input() const
{
return m_data.second.toString();
}
QString Tomahawk::EchonestControl::match() const
QString
Tomahawk::EchonestControl::match() const
{
return m_matchData;
}
QString Tomahawk::EchonestControl::matchString()
QString
Tomahawk::EchonestControl::matchString() const
{
return m_matchString;
}
QString
Tomahawk::EchonestControl::summary() const
{
return m_summary;
}
void Tomahawk::EchonestControl::setInput(const QString& input)
void
Tomahawk::EchonestControl::setInput(const QString& input)
{
// TODO generate widgets
m_data.second = input;
updateWidgetsFromData();
}
void Tomahawk::EchonestControl::setMatch(const QString& match)
void
Tomahawk::EchonestControl::setMatch(const QString& match)
{
// TODO generate widgets
m_matchData = match;
@ -295,6 +305,8 @@ Tomahawk::EchonestControl::updateWidgets()
m_match = QWeakPointer<QWidget>( new QWidget );
m_input = QWeakPointer<QWidget>( new QWidget );
}
calculateSummary();
}
void
@ -368,6 +380,8 @@ Tomahawk::EchonestControl::updateData()
m_data.second = enumVal;
}
}
calculateSummary();
}
void
@ -428,6 +442,7 @@ Tomahawk::EchonestControl::updateWidgetsFromData()
input->setCurrentIndex( val );
}
}
calculateSummary();
}
void
@ -449,10 +464,69 @@ void Tomahawk::EchonestControl::updateToLabelAndCombo()
}
}
void
Tomahawk::EchonestControl::editingFinished()
{
qDebug() << Q_FUNC_INFO;
m_editingTimer.start();
}
void
Tomahawk::EchonestControl::calculateSummary()
{
// turns the current control into an english phrase suitable for embedding into a sentence summary
QString summary;
if( selectedType() == "Artist" ) {
// magic char is used by EchonestGenerator to split the prefix from the artist name
if( static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( m_matchData.toInt() ) == Echonest::DynamicPlaylist::ArtistType )
summary = QString( "only by ~%1" ).arg( m_data.second.toString() );
else if( static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( m_matchData.toInt() ) == Echonest::DynamicPlaylist::ArtistRadioType )
summary = QString( "similar to ~%1" ).arg( m_data.second.toString() );
else if( static_cast< Echonest::DynamicPlaylist::ArtistTypeEnum >( m_matchData.toInt() ) == Echonest::DynamicPlaylist::ArtistDescriptionType )
summary = QString( "like ~%1" ).arg( m_data.second.toString() );
} else if( selectedType() == "Variety" || selectedType() == "Danceability" || selectedType() == "Artist Hotttnesss" || selectedType() == "Energy" || selectedType() == "Artist Familiarity" || selectedType() == "Song Hotttnesss" ) {
QString modifier;
qreal sliderVal = m_data.second.toReal();
// divide into avpproximate chunks
if( 0.0 <= sliderVal && sliderVal < 0.2 )
modifier = "very low";
else if( 0.2 <= sliderVal && sliderVal < 0.4 )
modifier = "low";
else if( 0.4 <= sliderVal && sliderVal < 0.6 )
modifier = "moderate";
else if( 0.6 <= sliderVal && sliderVal < 0.8 )
modifier = "high";
else if( 0.8 <= sliderVal && sliderVal <= 1 )
modifier = "very high";
summary = QString( "with %1 %2" ).arg( modifier ).arg( selectedType().toLower() );
} else if( selectedType() == "Tempo" ) {
summary = QString( "about %1 BPM" ).arg( m_data.second.toString() );
} else if( selectedType() == "Duration" ) {
summary = QString( "about %1 minutes long" ).arg( m_data.second.toString() );
} else if( selectedType() == "Loudness" ) {
summary = QString( "about %1 dB" ).arg( m_data.second.toString() );
} else if( selectedType() == "Latitude" || selectedType() == "Longitude" ) {
summary = QString( "at around %1%2 %3" ).arg( m_data.second.toString() ).arg( QString( QChar( 0x00B0 ) ) ).arg( selectedType().toLower() );
} else if( selectedType() == "Key" ) {
Q_ASSERT( !m_input.isNull() );
Q_ASSERT( qobject_cast< QComboBox* >( m_input.data() ) );
QString keyName = qobject_cast< QComboBox* >( m_input.data() )->currentText().toLower();
summary = QString( "in %1" ).arg( keyName );
} else if( selectedType() == "Mode" ) {
Q_ASSERT( !m_input.isNull() );
Q_ASSERT( qobject_cast< QComboBox* >( m_input.data() ) );
QString modeName = qobject_cast< QComboBox* >( m_input.data() )->currentText().toLower();
summary = QString( "in a %1 key" ).arg( modeName );
} else if( selectedType() == "Sorting" ) {
Q_ASSERT( !m_input.isNull() );
Q_ASSERT( qobject_cast< QComboBox* >( m_input.data() ) );
QString sortType = qobject_cast< QComboBox* >( m_input.data() )->currentText().toLower();
Q_ASSERT( !m_match.isNull() );
Q_ASSERT( qobject_cast< QComboBox* >( m_match.data() ) );
QString ascdesc = qobject_cast< QComboBox* >( m_match.data() )->currentText().toLower();
summary = QString( "sorted in %1 %2 order" ).arg( ascdesc ).arg( sortType );
}
m_summary = summary;
}

View File

@ -37,11 +37,12 @@ public:
virtual QString input() const;
virtual QString match() const;
virtual QString matchString();
virtual QString matchString() const;
virtual QString summary() const;
virtual void setInput(const QString& input);
virtual void setMatch(const QString& match);
/// DO NOT USE IF YOU ARE NOT A DBCMD
explicit EchonestControl( const QString& type, const QStringList& typeSelectors, QObject* parent = 0 );
@ -64,6 +65,8 @@ private:
void updateToComboAndSlider( bool smooth = false );
void updateToLabelAndCombo();
void calculateSummary();
Echonest::DynamicPlaylist::PlaylistParam m_currentType;
int m_overrideType;
@ -71,6 +74,7 @@ private:
QWeakPointer< QWidget > m_match;
QString m_matchData;
QString m_matchString;
QString m_summary;
QTimer m_editingTimer;

View File

@ -224,14 +224,16 @@ void
EchonestGenerator::appendRadioType( Echonest::DynamicPlaylist::PlaylistParams& params ) const throw( std::runtime_error )
{
/**
* so we try to match the best type of echonest playlist, based on the controls
* the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones.
* So we try to match the best type of echonest playlist, based on the controls
* the types are artist, artist-radio, artist-description, catalog, catalog-radio, song-radio. we don't care about the catalog ones, and
* we can't use the song ones since for the moment EN only accepts Song IDs, not names, and we don't want to insert an extra song.search
* call first.
*
*/
/// 1. artist: If all the artist controls are Limit-To. If some were but not all, error out.
/// 2. artist-description: If all the artist entries are Description. If some were but not all, error out.
/// 3. Artist-Radio: If all the artist entries are Similar To. If some were but not all, error out.
/// 3. artist-radio: If all the artist entries are Similar To. If some were but not all, error out.
if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistType ) )
params.append( Echonest::DynamicPlaylist::PlaylistParamData( Echonest::DynamicPlaylist::Type, Echonest::DynamicPlaylist::ArtistType ) );
else if( onlyThisArtistType( Echonest::DynamicPlaylist::ArtistDescriptionType ) )
@ -249,3 +251,80 @@ EchonestGenerator::queryFromSong(const Echonest::Song& song)
track[ "track" ] = song.title();
return query_ptr( new Query( track ) );
}
QString
EchonestGenerator::sentenceSummary()
{
/**
* The idea is we generate an english sentence from the individual phrases of the controls. We have to follow a few rules, but othewise it's quite straightforward.
*
* Rules:
* - Sentence starts with "Songs "
* - Artists always go first
* - Separate phrases by comma, and before last phrase
* - sorting always at end
* - collapse artists. "Like X, like Y, like Z, ..." -> "Like X, Y, and Z"
*
* NOTE / TODO: In order for the sentence to be grammatically correct, we must follow the EN API rules. That means we can't have multiple of some types of filters,
* and all Artist types must be the same. The filters aren't checked at the moment until Generate / Play is pressed. Consider doing a check on hide as well.
*/
QList< dyncontrol_ptr > allcontrols = m_controls;
QString sentence = "Songs ";
/// 1. Collect all artist filters
/// 2. Get the sorted by filter if it exists.
QList< dyncontrol_ptr > artists;
dyncontrol_ptr sorting;
foreach( const dyncontrol_ptr& control, allcontrols ) {
if( control->selectedType() == "Artist" )
artists << control;
else if( control->selectedType() == "Sorting" )
sorting = control;
}
if( !sorting.isNull() )
allcontrols.removeAll( sorting );
/// Do the assembling. Start with the artists if there are any, then do all the rest.
for( int i = 0; i < artists.size(); i++ ) {
dyncontrol_ptr artist = artists.value( i );
allcontrols.removeAll( artist ); // remove from pool while we're here
/// Collapse artist lists
QString center, suffix;
QString summary = artist.dynamicCast< EchonestControl >()->summary();
if( i == 0 ) { // if it's the first and only one
center = summary.remove( "~" );
if( artists.size() == 2 ) // special case for 2, no comma. ( X and Y )
suffix = " and ";
else
suffix = ", ";
} else {
center = summary.mid( summary.indexOf( "~" ) + 1 );
suffix = ", ";
if( i != artists.size() - 1 ) // if there are more, add an " and "
suffix += "and ";
}
sentence += center + suffix;
}
qDebug() << "Got artists:" << sentence;
for( int i = 0; i < allcontrols.size(); i++ ) {
// end case: if this is the last AND there is not a sorting filter (so this is the real last one)
const bool last = ( i == allcontrols.size() - 1 && sorting.isNull() );
QString prefix, suffix;
if( last ) {
prefix = "and ";
suffix = ".";
} else
suffix = ", ";
sentence += prefix + allcontrols.value( i ).dynamicCast< EchonestControl >()->summary() + suffix;
}
qDebug() << "Got artists and contents:" << sentence;
if( !sorting.isNull() ) {
sentence += "and " + sorting.dynamicCast< EchonestControl >()->summary() + ".";
}
qDebug() << "Got full summary:" << sentence;
return sentence;
}

View File

@ -49,6 +49,7 @@ public:
virtual void generate ( int number = -1 );
virtual void startOnDemand();
virtual void fetchNext( int rating = -1 );
virtual QString sentenceSummary();
private slots:
void staticFinished();

View File

@ -16,6 +16,7 @@
#include "CollapsibleControls.h"
#include "tomahawk/tomahawkapp.h"
#include "DynamicControlList.h"
#include "DynamicControlWrapper.h"
#include "dynamic/GeneratorInterface.h"
@ -23,6 +24,8 @@
#include <QLabel>
#include <QStackedLayout>
#include <QToolButton>
#include <QAction>
using namespace Tomahawk;
@ -32,11 +35,12 @@ CollapsibleControls::CollapsibleControls( QWidget* parent )
init();
}
CollapsibleControls::CollapsibleControls( const geninterface_ptr& generator, const QList< dyncontrol_ptr >& controls, bool isLocal, QWidget* parent )
CollapsibleControls::CollapsibleControls( const dynplaylist_ptr& playlist, bool isLocal, QWidget* parent )
: QWidget( parent )
, m_dynplaylist( playlist )
{
init();
setControls( generator, controls, isLocal );
setControls( m_dynplaylist, isLocal );
}
Tomahawk::CollapsibleControls::~CollapsibleControls()
@ -54,16 +58,28 @@ CollapsibleControls::init()
m_controls = new Tomahawk::DynamicControlList( this );
m_layout->addWidget( m_controls );
connect( m_controls, SIGNAL( toggleCollapse() ), this, SLOT( toggleCollapse() ) );
m_summaryWidget = new QWidget( this );
// TODO replace
// m_summaryWidget->setMinimumHeight( 24 );
// m_summaryWidget->setMaximumHeight( 24 );
m_summaryWidget->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed );
m_summaryWidget->setLayout( new QVBoxLayout );
QHBoxLayout* summaryLayout = new QHBoxLayout;
m_summaryWidget->setLayout( summaryLayout );
m_summaryWidget->layout()->setMargin( 0 );
m_summaryWidget->layout()->addWidget( new QLabel( "replace me plz", m_summaryWidget ) );
m_summary = new QLabel( m_summaryWidget );
summaryLayout->addWidget( m_summary, 1 );
m_summaryExpand = new QToolButton( m_summary );
m_summaryExpand->setIconSize( QSize( 16, 16 ) );
m_summaryExpand->setIcon( QIcon( RESPATH "images/arrow-down-double.png" ) );
m_summaryExpand->setToolButtonStyle( Qt::ToolButtonIconOnly );
m_summaryExpand->setAutoRaise( true );
m_summaryExpand->setContentsMargins( 0, 0, 0, 0 );
summaryLayout->addWidget( m_summaryExpand );
m_layout->addWidget( m_summaryWidget );
connect( m_summaryExpand, SIGNAL( clicked( bool ) ), this, SLOT( toggleCollapse() ) );
m_layout->setCurrentIndex( 0 );
connect( m_controls, SIGNAL( controlChanged( Tomahawk::dyncontrol_ptr ) ), SIGNAL( controlChanged( Tomahawk::dyncontrol_ptr ) ) );
@ -82,13 +98,19 @@ Tomahawk::CollapsibleControls::controls() const
}
void
CollapsibleControls::setControls( const geninterface_ptr& generator, const QList< dyncontrol_ptr >& controls, bool isLocal )
CollapsibleControls::setControls( const dynplaylist_ptr& playlist, bool isLocal )
{
m_controls->setControls( generator, controls, isLocal );
m_dynplaylist = playlist;
m_controls->setControls( m_dynplaylist->generator(), m_dynplaylist->generator()->controls(), isLocal );
}
void
CollapsibleControls::toggleCollapse()
{
if( m_layout->currentWidget() == m_controls ) {
m_summary->setText( m_dynplaylist->generator()->sentenceSummary() );
m_layout->setCurrentWidget( m_summaryWidget );
} else {
m_layout->setCurrentWidget( m_controls );
}
}

View File

@ -21,6 +21,8 @@
#include <QWidget>
class QToolButton;
class QLabel;
class QStackedLayout;
namespace Tomahawk
{
@ -33,10 +35,10 @@ class CollapsibleControls : public QWidget
Q_OBJECT
public:
CollapsibleControls( QWidget* parent );
CollapsibleControls( const geninterface_ptr& generator, const QList< dyncontrol_ptr >& controls, bool isLocal, QWidget* parent = 0 );
CollapsibleControls( const dynplaylist_ptr& playlist, bool isLocal, QWidget* parent = 0 );
virtual ~CollapsibleControls();
void setControls( const geninterface_ptr& generator, const QList< dyncontrol_ptr >& controls, bool isLocal );
void setControls( const dynplaylist_ptr& playlist, bool isLocal );
QList< DynamicControlWrapper* > controls() const;
signals:
@ -49,10 +51,13 @@ private slots:
private:
void init();
dynplaylist_ptr m_dynplaylist;
QStackedLayout* m_layout;
DynamicControlList* m_controls;
QWidget* m_summaryWidget;
QWidget* m_summaryWidget;
QLabel* m_summary;
QToolButton* m_summaryExpand;
};
}

View File

@ -118,7 +118,7 @@ DynamicWidget::loadDynamicPlaylist( const Tomahawk::dynplaylist_ptr& playlist )
foreach( const dyncontrol_ptr& control, playlist->generator()->controls() ) {
qDebug() << "CONTROL:" << control->selectedType() << control->match() << control->input();
}
m_controls->setControls( m_playlist->generator(), m_playlist->generator()->controls(), m_playlist->author()->isLocal() );
m_controls->setControls( m_playlist, m_playlist->author()->isLocal() );
m_playlist = playlist;
m_view->setOnDemand( m_playlist->mode() == OnDemand );
@ -139,7 +139,7 @@ DynamicWidget::loadDynamicPlaylist( const Tomahawk::dynplaylist_ptr& playlist )
m_model->loadPlaylist( m_playlist );
if( !m_playlist.isNull() )
m_controls->setControls( m_playlist->generator(), m_playlist->generator()->controls(), m_playlist->author()->isLocal() );
m_controls->setControls( m_playlist, m_playlist->author()->isLocal() );
m_generatorCombo->setWritable( playlist->author()->isLocal() );
m_generatorCombo->setLabel( qobject_cast< QComboBox* >( m_generatorCombo->writableWidget() )->currentText() );