/src/libtomahawk/playlist/dynamic/widgets/DynamicWidget.cpp
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}