PageRenderTime 940ms CodeModel.GetById 71ms app.highlight 810ms RepoModel.GetById 52ms app.codeStats 1ms

/src/libtomahawk/playlist/dynamic/widgets/DynamicWidget.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 562 lines | 409 code | 116 blank | 37 comment | 72 complexity | 1c5070d0ae7e97f17f6cf7cdc9d4f529 MD5 | raw file
  1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2 *
  3 *   Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
  4 *   Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
  5 *
  6 *   Tomahawk is free software: you can redistribute it and/or modify
  7 *   it under the terms of the GNU General Public License as published by
  8 *   the Free Software Foundation, either version 3 of the License, or
  9 *   (at your option) any later version.
 10 *
 11 *   Tomahawk is distributed in the hope that it will be useful,
 12 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 13 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14 *   GNU General Public License for more details.
 15 *
 16 *   You should have received a copy of the GNU General Public License
 17 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 18 */
 19
 20#include "DynamicWidget.h"
 21
 22#include "DynamicControlList.h"
 23#include "playlist/dynamic/DynamicModel.h"
 24#include "playlist/PlayableProxyModel.h"
 25#include "playlist/PlayableItem.h"
 26#include "playlist/dynamic/GeneratorInterface.h"
 27#include "playlist/dynamic/GeneratorFactory.h"
 28#include "Pipeline.h"
 29#include "Source.h"
 30#include "audio/AudioEngine.h"
 31#include "ReadOrWriteWidget.h"
 32#include "CollapsibleControls.h"
 33#include "DynamicControlWrapper.h"
 34#include "ViewManager.h"
 35#include "playlist/dynamic/DynamicView.h"
 36#include "DynamicSetupWidget.h"
 37#include "widgets/BasicHeader.h"
 38#include "utils/AnimatedSpinner.h"
 39#include "utils/TomahawkUtilsGui.h"
 40#include "utils/Logger.h"
 41#include "utils/DpiScaler.h"
 42
 43#include <QVBoxLayout>
 44#include <QLabel>
 45#include <QComboBox>
 46#include <QPushButton>
 47#include <QSpinBox>
 48#include <QEvent>
 49#include <QPainter>
 50
 51using namespace Tomahawk;
 52
 53
 54DynamicWidget::DynamicWidget( const Tomahawk::dynplaylist_ptr& playlist, QWidget* parent )
 55    : QWidget( parent )
 56    , m_layout( new QVBoxLayout )
 57    , m_resolveOnNextLoad( false )
 58    , m_seqRevLaunched( 0 )
 59    , m_activePlaylist( false )
 60    , m_setup( 0 )
 61    , m_runningOnDemand( false )
 62    , m_controlsChanged( false )
 63    , m_steering( 0 )
 64    , m_controls( 0 )
 65    , m_view( 0 )
 66    , m_model()
 67{
 68    m_header = new BasicHeader;
 69    m_layout->addWidget( m_header );
 70
 71    m_controls = new CollapsibleControls( this );
 72    m_layout->addWidget( m_controls );
 73    setContentsMargins( 0, 0, 0, 1 ); // to align the bottom with the bottom of the sourcelist
 74
 75    m_model = new DynamicModel( this );
 76    m_view = new DynamicView( this );
 77    m_view->setDynamicModel( m_model );
 78    m_view->setContentsMargins( 0, 0, 0, 0 );
 79    m_layout->addWidget( m_view, 1 );
 80
 81    connect( m_model, SIGNAL( collapseFromTo( int, int ) ), m_view, SLOT( collapseEntries( int, int ) ) );
 82    connect( m_model, SIGNAL( trackGenerationFailure( QString ) ), this, SLOT( stationFailed( QString ) ) );
 83
 84    m_loading = new AnimatedSpinner( m_view );
 85    connect( m_model, SIGNAL( tracksAdded() ), m_loading, SLOT( fadeOut() ) );
 86
 87    m_setup = new DynamicSetupWidget( playlist, this );
 88    m_setup->fadeIn();
 89
 90    connect( m_model, SIGNAL( tracksAdded() ), this, SLOT( tracksAdded() ) );
 91
 92    loadDynamicPlaylist( playlist );
 93
 94    m_layout->setContentsMargins( 0, 0, 0, 0 );
 95    m_layout->setMargin( 0 );
 96    m_layout->setSpacing( 0 );
 97    setLayout( m_layout );
 98
 99    connect( m_setup, SIGNAL( generatePressed( int ) ), this, SLOT( generate( int ) ) );
100    connect( m_setup, SIGNAL( typeChanged( QString ) ), this, SLOT( playlistTypeChanged( QString ) ) );
101
102    layoutFloatingWidgets();
103
104    connect( m_controls, SIGNAL( controlChanged( Tomahawk::dyncontrol_ptr ) ), this, SLOT( controlChanged( Tomahawk::dyncontrol_ptr ) ), Qt::QueuedConnection );
105    connect( m_controls, SIGNAL( controlsChanged( bool ) ), this, SLOT( controlsChanged( bool ) ), Qt::QueuedConnection );
106
107    connect( AudioEngine::instance(), SIGNAL( started( Tomahawk::result_ptr ) ), this, SLOT( trackStarted() ) );
108    connect( AudioEngine::instance(), SIGNAL( playlistChanged( Tomahawk::playlistinterface_ptr ) ), this, SLOT( playlistChanged( Tomahawk::playlistinterface_ptr ) ) );
109}
110
111
112DynamicWidget::~DynamicWidget()
113{
114}
115
116
117dynplaylist_ptr
118DynamicWidget::playlist()
119{
120    return m_playlist;
121}
122
123
124void
125DynamicWidget::loadDynamicPlaylist( const Tomahawk::dynplaylist_ptr& playlist )
126{
127    // special case: if we have launched multiple setRevision calls, and the number of controls is different, it means that we're getting an intermediate setRevision
128    //  called after the user has already created more revisions. ignore in that case.
129    if ( m_playlist.data() == playlist.data() && m_seqRevLaunched > 0
130        && m_controls->controls().size() != playlist->generator()->controls().size() // different number of controls
131        && qAbs( m_playlist->generator()->controls().size() - playlist->generator()->controls().size() ) < m_seqRevLaunched )
132    { // difference in controls has to be less than how many revisions we launched
133        return;
134    }
135    m_seqRevLaunched = 0;
136
137    // if we're being told to load the same dynamic playlist over again, only do it if the controls have a different number
138    if ( !m_playlist.isNull() && ( m_playlist.data() == playlist.data() ) // same playlist pointer
139        && m_playlist->generator()->controls().size() == playlist->generator()->controls().size() )
140    {
141        // we can skip our work. just let the dynamiccontrollist show the difference
142        m_controls->setControls( m_playlist, m_playlist->author()->isLocal() );
143
144        m_playlist = playlist;
145
146        if ( !m_runningOnDemand )
147            m_model->loadPlaylist( m_playlist );
148        else if ( !m_controlsChanged ) // if the controls changed, we already dealt with that and don't want to change station yet
149            m_model->changeStation();
150        m_controlsChanged = false;
151
152        return;
153    }
154
155    if ( !m_playlist.isNull() )
156    {
157        disconnect( m_playlist->generator().data(), SIGNAL( generated( QList<Tomahawk::query_ptr> ) ), this, SLOT( tracksGenerated( QList<Tomahawk::query_ptr> ) ) );
158        disconnect( m_playlist.data(), SIGNAL( dynamicRevisionLoaded( Tomahawk::DynamicPlaylistRevision) ), this, SLOT(onRevisionLoaded( Tomahawk::DynamicPlaylistRevision) ) );
159        disconnect( m_playlist->generator().data(), SIGNAL( error( QString, QString ) ), this, SLOT( generatorError( QString, QString ) ) );
160        disconnect( m_playlist.data(), SIGNAL( deleted( Tomahawk::dynplaylist_ptr ) ), this, SLOT( onDeleted() ) );
161        disconnect( m_playlist.data(), SIGNAL( changed() ), this, SLOT( onChanged() ) );
162    }
163
164    m_playlist = playlist;
165    m_view->setOnDemand( m_playlist->mode() == OnDemand );
166    m_view->setReadOnly( !m_playlist->author()->isLocal() );
167    m_model->loadPlaylist( m_playlist );
168    m_controlsChanged = false;
169    m_setup->setPlaylist( m_playlist );
170    m_header->setCaption( m_playlist->title() );
171
172    if ( !m_playlist->author()->isLocal() )  // hide controls, as we show the description in the summary
173            m_layout->removeWidget( m_controls );
174    else if ( m_layout->indexOf( m_controls ) == -1 )
175        m_layout->insertWidget( 0, m_controls );
176
177    connect( m_playlist->generator().data(), SIGNAL( generated( QList<Tomahawk::query_ptr> ) ), this, SLOT( tracksGenerated( QList<Tomahawk::query_ptr> ) ) );
178    connect( m_playlist.data(), SIGNAL( dynamicRevisionLoaded( Tomahawk::DynamicPlaylistRevision ) ), this, SLOT( onRevisionLoaded( Tomahawk::DynamicPlaylistRevision ) ) );
179    connect( m_playlist->generator().data(), SIGNAL( error( QString, QString ) ), this, SLOT( generatorError( QString, QString ) ) );
180    connect( m_playlist.data(), SIGNAL( deleted( Tomahawk::dynplaylist_ptr ) ), this, SLOT( onDeleted() ) );
181    connect( m_playlist.data(), SIGNAL( changed() ), this, SLOT( onChanged() ) );
182
183    if ( m_playlist->mode() == OnDemand && !m_playlist->generator()->controls().isEmpty() )
184        showPreview();
185
186    if ( !m_playlist.isNull() )
187        m_controls->setControls( m_playlist, m_playlist->author()->isLocal() );
188}
189
190
191void
192DynamicWidget::onRevisionLoaded( const Tomahawk::DynamicPlaylistRevision& rev )
193{
194    Q_UNUSED( rev );
195    tDebug( LOGVERBOSE ) << "DynamicWidget::onRevisionLoaded" << rev.revisionguid;
196    if ( m_model->ignoreRevision( rev.revisionguid ) )
197    {
198        m_model->removeRevisionFromIgnore( rev.revisionguid );
199        return;
200    }
201
202    loadDynamicPlaylist( m_playlist );
203    if( m_resolveOnNextLoad || !m_playlist->author()->isLocal() )
204    {
205        m_playlist->resolve();
206        m_resolveOnNextLoad = false;
207    }
208}
209
210
211Tomahawk::playlistinterface_ptr
212DynamicWidget::playlistInterface() const
213{
214    return m_view->proxyModel()->playlistInterface();
215}
216
217
218QSize
219DynamicWidget::sizeHint() const
220{
221    // We want to take up as much room as the animated splitter containing us and the queue editor will allow. So we return a bogus huge sizehint
222    //  to avoid having to calculate it which is slow
223    return QSize( 5000, 5000 );
224}
225
226
227void
228DynamicWidget::resizeEvent( QResizeEvent* )
229{
230    layoutFloatingWidgets();
231}
232
233
234void
235DynamicWidget::layoutFloatingWidgets()
236{
237    if ( !m_runningOnDemand )
238    {
239        int x = ( width() / 2 ) - ( m_setup->size().width() / 2 );
240        int y = height() - m_setup->size().height() - 40; // padding
241
242        m_setup->move( x, y );
243    }
244    else if( m_runningOnDemand && m_steering )
245    {
246        int x = ( width() / 2 ) - ( m_steering->size().width() / 2 );
247        int y = height() - m_steering->size().height() - 40; // padding
248
249        m_steering->move( x, y );
250    }
251}
252
253
254void
255DynamicWidget::playlistChanged( Tomahawk::playlistinterface_ptr pl )
256{
257    if ( pl == m_view->proxyModel()->playlistInterface() ) // same playlist
258        m_activePlaylist = true;
259    else
260    {
261        m_activePlaylist = false;
262
263        // user started playing something somewhere else, so give it a rest
264        if ( m_runningOnDemand )
265        {
266            stopStation( false );
267        }
268    }
269}
270
271
272void
273DynamicWidget::showEvent( QShowEvent* )
274{
275    if ( !m_playlist.isNull() && !m_runningOnDemand )
276        m_setup->fadeIn();
277}
278
279
280void
281DynamicWidget::generate( int num )
282{
283    // get the items from the generator, and put them in the playlist
284    m_view->setDynamicWorking( true );
285    m_loading->fadeIn();
286    m_playlist->generator()->generate( num );
287}
288
289
290void
291DynamicWidget::stationFailed( const QString& msg )
292{
293    m_view->setDynamicWorking( false );
294    m_view->showMessage( msg );
295    m_loading->fadeOut();
296
297    stopStation( false );
298}
299
300
301void
302DynamicWidget::trackStarted()
303{
304    if ( m_activePlaylist && !m_playlist.isNull() &&
305        m_playlist->mode() == OnDemand && !m_runningOnDemand )
306    {
307
308        startStation();
309    }
310}
311
312
313void
314DynamicWidget::tracksAdded()
315{
316    if ( m_playlist->mode() == OnDemand && m_runningOnDemand && m_setup->isVisible() )
317        m_setup->fadeOut();
318}
319
320
321void
322DynamicWidget::stopStation( bool stopPlaying )
323{
324    m_model->stopOnDemand( stopPlaying );
325    m_runningOnDemand = false;
326
327    // TODO until i add a qwidget interface
328    QMetaObject::invokeMethod( m_steering, "fadeOut", Qt::DirectConnection );
329    m_setup->fadeIn();
330}
331
332
333void
334DynamicWidget::startStation()
335{
336    m_runningOnDemand = true;
337    m_model->startOnDemand();
338
339    m_setup->fadeOut();
340    // show the steering controls
341    if ( m_playlist->generator()->onDemandSteerable() )
342    {
343        // position it horizontally centered, above the botton.
344        m_steering = m_playlist->generator()->steeringWidget();
345        Q_ASSERT( m_steering );
346
347        connect( m_steering, SIGNAL( steeringChanged() ), this, SLOT( steeringChanged() ) );
348
349        int x = ( width() / 2 ) - ( m_steering->size().width() / 2 );
350        int y = height() - m_steering->size().height() - 40; // padding
351
352        m_steering->setParent( this );
353        m_steering->move( x, y );
354
355        // TODO until i add a qwidget interface
356        QMetaObject::invokeMethod( m_steering, "fadeIn", Qt::DirectConnection );
357
358        connect( m_steering, SIGNAL( resized() ), this, SLOT( layoutFloatingWidgets() ) );
359    }
360}
361
362
363void
364DynamicWidget::playlistTypeChanged( QString )
365{
366    // TODO
367}
368
369
370void
371DynamicWidget::tracksGenerated( const QList< query_ptr >& queries )
372{
373    int limit = -1; // only limit the "preview" of a station
374    if ( m_playlist->author()->isLocal() && m_playlist->mode() == Static )
375    {
376        m_resolveOnNextLoad = true;
377    }
378    else if ( m_playlist->mode() == OnDemand )
379    {
380        limit = 5;
381    }
382
383    if ( m_playlist->mode() != OnDemand )
384        m_loading->fadeOut();
385    m_model->tracksGenerated( queries, limit );
386}
387
388
389void
390DynamicWidget::controlsChanged( bool added )
391{
392    // controlsChanged() is emitted when a control is added or removed
393    // in the case of addition, it's blank by default... so to avoid an error
394    // when playing a station just ignore it till we're ready and get a controlChanged()
395    m_controlsChanged = true;
396
397    if ( !m_playlist->author()->isLocal() )
398        return;
399    m_playlist->createNewRevision();
400    m_seqRevLaunched++;
401
402    if ( !added )
403        showPreview();
404
405    emit descriptionChanged( m_playlist->generator()->sentenceSummary() );
406}
407
408
409void
410DynamicWidget::controlChanged( const Tomahawk::dyncontrol_ptr& control )
411{
412    Q_UNUSED( control );
413    if ( !m_playlist->author()->isLocal() )
414        return;
415    m_playlist->createNewRevision();
416    m_seqRevLaunched++;
417
418    showPreview();
419
420    emit descriptionChanged( m_playlist->generator()->sentenceSummary() );
421}
422
423
424void
425DynamicWidget::steeringChanged()
426{
427    // When steering changes, toss all the tracks that are upcoming, and re-fetch.
428    // We have to find the currently playing item
429    QModelIndex playing;
430    for ( int i = 0; i < m_view->proxyModel()->rowCount( QModelIndex() ); ++i )
431    {
432        const QModelIndex  cur = m_view->proxyModel()->index( i, 0, QModelIndex() );
433        PlayableItem* item = m_view->proxyModel()->itemFromIndex( m_view->proxyModel()->mapToSource( cur ) );
434        if ( item && item->isPlaying() )
435        {
436            playing = cur;
437            break;
438        }
439    }
440    if ( !playing.isValid() )
441        return;
442
443    const int upcoming = m_view->proxyModel()->rowCount( QModelIndex() ) - 1 - playing.row();
444    tDebug() << "Removing tracks after current in station, found" << upcoming;
445
446    QModelIndexList toRemove;
447    for ( int i = playing.row() + 1; i < m_view->proxyModel()->rowCount( QModelIndex() ); i++ )
448    {
449        toRemove << m_view->proxyModel()->index( i, 0, QModelIndex() );
450    }
451
452    m_view->proxyModel()->removeIndexes( toRemove );
453
454    m_playlist->generator()->fetchNext();
455}
456
457
458void
459DynamicWidget::showPreview()
460{
461    if ( m_playlist->mode() == OnDemand && !m_runningOnDemand )
462    {
463        // if this is a not running station, preview matching tracks
464        m_model->clear();
465        generate( 20 ); // ask for more, we'll filter how many we actually want
466    }
467}
468
469
470void
471DynamicWidget::generatorError( const QString& title, const QString& content )
472{
473    m_view->setDynamicWorking( false );
474    m_loading->fadeOut();
475
476    if ( m_runningOnDemand )
477    {
478        stopStation( false );
479        m_view->showMessage( tr( "Station ran out of tracks!\n\nTry tweaking the filters for a new set of songs to play." ) );
480    }
481    else
482        m_view->showMessageTimeout( title, content );
483}
484
485
486void
487DynamicWidget::paintRoundedFilledRect( QPainter& p, QPalette& /* pal */, QRect& r, qreal opacity )
488{
489    p.setBackgroundMode( Qt::TransparentMode );
490    p.setRenderHint( QPainter::Antialiasing );
491    p.setOpacity( opacity );
492
493    QColor c( 30, 30, 30 );
494
495    QPen pen( c.darker(), .5 );
496    p.setPen( pen );
497    p.setBrush( c );
498
499    p.drawRoundedRect( r, 10, 10 );
500
501    p.setOpacity( opacity + .2 );
502    p.setBrush( QBrush() );
503    p.setPen( pen );
504    p.drawRoundedRect( r, 10, 10 );
505}
506
507
508QString
509DynamicWidget::description() const
510{
511    return m_model->description();
512}
513
514
515QString
516DynamicWidget::title() const
517{
518    return m_model->title();
519}
520
521
522QPixmap
523DynamicWidget::pixmap() const
524{
525    if ( m_playlist->mode() == OnDemand )
526        return TomahawkUtils::defaultPixmap( TomahawkUtils::Station,
527                                             TomahawkUtils::Original,
528                                             TomahawkUtils::DpiScaler::scaled( this, 80, 80 ) );
529    else if ( m_playlist->mode() == Static )
530        return TomahawkUtils::defaultPixmap( TomahawkUtils::AutomaticPlaylist,
531                                             TomahawkUtils::Original,
532                                             TomahawkUtils::DpiScaler::scaled( this, 80, 80 ) );
533    else
534        return QPixmap();
535}
536
537
538bool
539DynamicWidget::jumpToCurrentTrack()
540{
541    m_view->scrollTo( m_view->proxyModel()->currentIndex(), QAbstractItemView::PositionAtCenter );
542    return true;
543}
544
545
546void
547DynamicWidget::onDeleted()
548{
549    emit destroyed( widget() );
550}
551
552
553void
554DynamicWidget::onChanged()
555{
556    if ( !m_playlist.isNull() &&
557         ViewManager::instance()->currentPage() == this )
558    {
559        m_header->setCaption( m_playlist->title() );
560        emit nameChanged( m_playlist->title() );
561    }
562}