/src/sourcetree/sourcesmodel.cpp
C++ | 683 lines | 485 code | 140 blank | 58 comment | 66 complexity | 5aebd121ef316f3fcadee48d9b3bab13 MD5 | raw file
1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> === 2 * 3 * Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org> 4 * Copyright 2010-2011, Leo Franchi <lfranchi@kde.org> 5 * Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org> 6 * 7 * Tomahawk is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 * 12 * Tomahawk is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with Tomahawk. If not, see <http://www.gnu.org/licenses/>. 19 */ 20 21#include "sourcetree/SourcesModel.h" 22 23#include "sourcetree/items/SourceTreeItem.h" 24#include "sourcetree/items/SourceItem.h" 25#include "sourcetree/items/GroupItem.h" 26#include "sourcetree/items/GenericPageItems.h" 27#include "sourcetree/items/HistoryItem.h" 28#include "sourcetree/items/LovedTracksItem.h" 29#include "SourceList.h" 30#include "Playlist.h" 31#include "Collection.h" 32#include "Source.h" 33#include "ViewManager.h" 34 35#include "utils/Logger.h" 36#include "GlobalActionManager.h" 37#include "DropJob.h" 38#include "items/PlaylistItems.h" 39#include "playlist/TreeView.h" 40#include "playlist/PlaylistView.h" 41#include "playlist/dynamic/widgets/DynamicWidget.h" 42 43#include <QMimeData> 44#include <QSize> 45 46#include <boost/bind.hpp> 47 48using namespace Tomahawk; 49 50 51SourcesModel::SourcesModel( QObject* parent ) 52 : QAbstractItemModel( parent ) 53 , m_rootItem( 0 ) 54 , m_viewPageDelayedCacheItem( 0 ) 55{ 56 m_rootItem = new SourceTreeItem( this, 0, Invalid ); 57 58 appendGroups(); 59 onSourcesAdded( SourceList::instance()->sources() ); 60 61 connect( SourceList::instance(), SIGNAL( sourceAdded( Tomahawk::source_ptr ) ), SLOT( onSourceAdded( Tomahawk::source_ptr ) ) ); 62 connect( SourceList::instance(), SIGNAL( sourceRemoved( Tomahawk::source_ptr ) ), SLOT( onSourceRemoved( Tomahawk::source_ptr ) ) ); 63 connect( ViewManager::instance(), SIGNAL( viewPageActivated( Tomahawk::ViewPage* ) ), this, SLOT( viewPageActivated( Tomahawk::ViewPage* ) ) ); 64} 65 66 67SourcesModel::~SourcesModel() 68{ 69 delete m_rootItem; 70} 71 72 73QString 74SourcesModel::rowTypeToString( RowType type ) 75{ 76 switch ( type ) 77 { 78 case Group: 79 return tr( "Group" ); 80 81 case Collection: 82 return tr( "Collection" ); 83 84 case StaticPlaylist: 85 return tr( "Playlist" ); 86 87 case AutomaticPlaylist: 88 return tr( "Automatic Playlist" ); 89 90 case Station: 91 return tr( "Station" ); 92 93 default: 94 return QString( "Unknown" ); 95 } 96} 97 98 99QVariant 100SourcesModel::data( const QModelIndex& index, int role ) const 101{ 102 if ( !index.isValid() ) 103 return QVariant(); 104 105 SourceTreeItem* item = itemFromIndex( index ); 106 if ( !item ) 107 return QVariant(); 108 109 switch ( role ) 110 { 111 case Qt::SizeHintRole: 112 return QSize( 0, 18 ); 113 case SourceTreeItemRole: 114 return QVariant::fromValue< SourceTreeItem* >( item ); 115 case SourceTreeItemTypeRole: 116 return item->type(); 117 case Qt::DisplayRole: 118 case Qt::EditRole: 119 return item->text(); 120 case Qt::DecorationRole: 121 return item->icon(); 122 case SourcesModel::SortRole: 123 return item->peerSortValue(); 124 case SourcesModel::IDRole: 125 return item->IDValue(); 126 case SourcesModel::LatchedOnRole: 127 { 128 if ( item->type() == Collection ) 129 { 130 SourceItem* cItem = qobject_cast< SourceItem* >( item ); 131 return cItem->localLatchedOn(); 132 } 133 return false; 134 } 135 case SourcesModel::LatchedRealtimeRole: 136 { 137 if ( item->type() == Collection ) 138 { 139 SourceItem* cItem = qobject_cast< SourceItem* >( item ); 140 return cItem->localLatchMode() == Tomahawk::PlaylistModes::RealTime; 141 } 142 return false; 143 } 144 case SourcesModel::CustomActionRole: 145 { 146 return QVariant::fromValue< QList< QAction* > >( item->customActions() ); 147 } 148 case Qt::ToolTipRole: 149 if ( !item->tooltip().isEmpty() ) 150 return item->tooltip(); 151 } 152 return QVariant(); 153} 154 155 156int 157SourcesModel::columnCount( const QModelIndex& ) const 158{ 159 return 1; 160} 161 162 163int 164SourcesModel::rowCount( const QModelIndex& parent ) const 165{ 166 if( !parent.isValid() ) 167 { 168 return m_rootItem->children().count(); 169 } 170 171 return itemFromIndex( parent )->children().count(); 172} 173 174 175QModelIndex 176SourcesModel::parent( const QModelIndex& child ) const 177{ 178 if( !child.isValid() ) 179 { 180 return QModelIndex(); 181 } 182 183 SourceTreeItem* node = itemFromIndex( child ); 184 SourceTreeItem* parent = node->parent(); 185 if( parent == m_rootItem ) 186 return QModelIndex(); 187 188 return createIndex( rowForItem( parent ), 0, parent ); 189} 190 191 192QModelIndex 193SourcesModel::index( int row, int column, const QModelIndex& parent ) const 194{ 195 if( row < 0 || column < 0 ) 196 return QModelIndex(); 197 198 if( hasIndex( row, column, parent ) ) 199 { 200 SourceTreeItem *parentNode = itemFromIndex( parent ); 201 SourceTreeItem *childNode = parentNode->children().at( row ); 202 return createIndex( row, column, childNode ); 203 } 204 205 return QModelIndex(); 206 207} 208 209 210bool 211SourcesModel::setData( const QModelIndex& index, const QVariant& value, int role ) 212{ 213 SourceTreeItem* item = itemFromIndex( index ); 214 return item->setData( value, role ); 215} 216 217 218QStringList 219SourcesModel::mimeTypes() const 220{ 221 return DropJob::mimeTypes(); 222} 223 224 225QMimeData* 226SourcesModel::mimeData( const QModelIndexList& ) const 227{ 228 // TODO 229 return new QMimeData(); 230} 231 232 233bool 234SourcesModel::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent ) 235{ 236 SourceTreeItem* item = 0; 237// qDebug() << "Got mime data dropped:" << row << column << parent << itemFromIndex( parent )->text(); 238 if( row == -1 && column == -1 ) 239 item = itemFromIndex( parent ); 240 else if( column == 0 ) 241 item = itemFromIndex( index( row, column, parent ) ); 242 else if( column == -1 ) // column is -1, that means the drop is happening "below" the indices. that means we actually want the one before it 243 item = itemFromIndex( index( row - 1, 0, parent ) ); 244 245 Q_ASSERT( item ); 246 247// qDebug() << "Dropping on:" << item->text(); 248 return item->dropMimeData( data, action ); 249} 250 251 252Qt::DropActions 253SourcesModel::supportedDropActions() const 254{ 255#ifdef Q_WS_MAC 256 return Qt::CopyAction | Qt::MoveAction; 257#else 258 return Qt::CopyAction; 259#endif 260} 261 262 263Qt::ItemFlags 264SourcesModel::flags( const QModelIndex& index ) const 265{ 266 if ( index.isValid() ) 267 { 268 return itemFromIndex( index )->flags(); 269 } 270 else 271 return 0; 272} 273 274 275void 276SourcesModel::appendGroups() 277{ 278 beginInsertRows( QModelIndex(), rowCount(), rowCount() + 3 ); 279 280 GroupItem* browse = new GroupItem( this, m_rootItem, tr( "Browse" ), 0 ); 281 new HistoryItem( this, m_rootItem, tr( "Search History" ), 1 ); 282// new SourceTreeItem( this, m_rootItem, SourcesModel::Divider, 2 ); 283 m_myMusicGroup = new GroupItem( this, m_rootItem, tr( "My Music" ), 3 ); 284 285 GenericPageItem* dashboard = new GenericPageItem( this, browse, tr( "Dashboard" ), QIcon( RESPATH "images/dashboard.png" ), 286 boost::bind( &ViewManager::showWelcomePage, ViewManager::instance() ), 287 boost::bind( &ViewManager::welcomeWidget, ViewManager::instance() ) ); 288 dashboard->setSortValue( 0 ); 289 290 // super collection 291 GenericPageItem* sc = new GenericPageItem( this, browse, tr( "SuperCollection" ), QIcon( RESPATH "images/supercollection.png" ), 292 boost::bind( &ViewManager::showSuperCollection, ViewManager::instance() ), 293 boost::bind( &ViewManager::superCollectionView, ViewManager::instance() ) ); 294 sc->setSortValue( 1 ); 295 296 // browse section 297 LovedTracksItem* loved = new LovedTracksItem( this, browse ); 298 loved->setSortValue( 2 ); 299 300 GenericPageItem* recent = new GenericPageItem( this, browse, tr( "Recently Played" ), QIcon( RESPATH "images/recently-played.png" ), 301 boost::bind( &ViewManager::showRecentPlaysPage, ViewManager::instance() ), 302 boost::bind( &ViewManager::recentPlaysWidget, ViewManager::instance() ) ); 303 recent->setSortValue( 3 ); 304 305 GenericPageItem* hot = new GenericPageItem( this, browse, tr( "Charts" ), QIcon( RESPATH "images/charts.png" ), 306 boost::bind( &ViewManager::showWhatsHotPage, ViewManager::instance() ), 307 boost::bind( &ViewManager::whatsHotWidget, ViewManager::instance() ) ); 308 hot->setSortValue( 4 ); 309 310 GenericPageItem* newReleases = new GenericPageItem( this, browse, tr( "New Releases" ), QIcon( RESPATH "images/new-releases.png" ), 311 boost::bind( &ViewManager::showNewReleasesPage, ViewManager::instance() ), 312 boost::bind( &ViewManager::newReleasesWidget, ViewManager::instance() ) ); 313 newReleases->setSortValue( 5 ); 314 315 m_collectionsGroup = new GroupItem( this, m_rootItem, tr( "Friends" ), 4 ); 316 317 endInsertRows(); 318} 319 320 321void 322SourcesModel::appendItem( const Tomahawk::source_ptr& source ) 323{ 324 GroupItem* parent; 325 if ( !source.isNull() && source->isLocal() ) 326 { 327 parent = m_myMusicGroup; 328 } 329 else 330 { 331 parent = m_collectionsGroup; 332 } 333 334 QModelIndex idx = indexFromItem( parent ); 335 beginInsertRows( idx, rowCount( idx ), rowCount( idx ) ); 336 new SourceItem( this, parent, source ); 337 endInsertRows(); 338 339 parent->checkExpandedState(); 340} 341 342 343bool 344SourcesModel::removeItem( const Tomahawk::source_ptr& source ) 345{ 346// qDebug() << "Removing source item from SourceTree:" << source->friendlyName(); 347 348 QModelIndex idx; 349 int rows = rowCount(); 350 for ( int row = 0; row < rows; row++ ) 351 { 352 QModelIndex idx = index( row, 0, QModelIndex() ); 353 SourceItem* item = static_cast< SourceItem* >( idx.internalPointer() ); 354 if ( item && item->source() == source ) 355 { 356// qDebug() << "Found removed source item:" << item->source()->userName(); 357 beginRemoveRows( QModelIndex(), row, row ); 358 m_rootItem->removeChild( item ); 359 endRemoveRows(); 360 361// onItemOffline( idx ); 362 363 delete item; 364 return true; 365 } 366 } 367 368 return false; 369} 370 371 372void 373SourcesModel::viewPageActivated( Tomahawk::ViewPage* page ) 374{ 375 if ( !m_sourcesWithViewPage.isEmpty() ) 376 { 377 // Hide again any offline sources we exposed, since we're showing a different page now. they'll be re-shown if the user selects a playlist that is from an offline user 378 QList< source_ptr > temp = m_sourcesWithViewPage; 379 m_sourcesWithViewPage.clear(); 380 foreach ( const source_ptr& s, temp ) 381 { 382 QModelIndex idx = indexFromItem( m_sourcesWithViewPageItems.value( s ) ); 383 emit dataChanged( idx, idx ); 384 } 385 m_sourcesWithViewPageItems.clear(); 386 } 387 388 if ( m_sourceTreeLinks.contains( page ) ) 389 { 390 Q_ASSERT( m_sourceTreeLinks[ page ] ); 391// qDebug() << "Got view page activated for item:" << m_sourceTreeLinks[ page ]->text(); 392 QModelIndex idx = indexFromItem( m_sourceTreeLinks[ page ] ); 393 394 if ( !idx.isValid() ) 395 m_sourceTreeLinks.remove( page ); 396 else 397 emit selectRequest( QPersistentModelIndex( idx ) ); 398 } 399 else 400 { 401 playlist_ptr p = ViewManager::instance()->playlistForPage( page ); 402 // HACK 403 // try to find it if it is a playlist. not pretty at all.... but this happens when ViewManager loads a playlist or dynplaylist NOT from the sidebar but from somewhere else 404 // we don't know which sourcetreeitem is related to it, so we have to find it. we also don't know if this page is a playlist or dynplaylist or not, but we can't check as we can't 405 // include DynamicWidget.h here (so can't dynamic_cast). 406 // this could also be fixed by keeping a master list of playlists/sourcetreeitems... but that's even uglier i think. this is only called the first time a certain viewpage is clicked from external 407 // sources. 408 SourceTreeItem* item = activatePlaylistPage( page, m_rootItem ); 409 m_viewPageDelayedCacheItem = page; 410 411 if ( !p.isNull() ) 412 { 413 source_ptr s= p->author(); 414 if ( !s.isNull() && !s->isOnline() && item ) 415 { 416 m_sourcesWithViewPage << s; 417 418 // show the collection now... yeah. 419 if ( !item->parent() || !item->parent()->parent() ) 420 { 421 tLog() << "Found playlist item with no category parent or collection parent!" << item->text(); 422 return; 423 } 424 425 SourceTreeItem* collectionOfPlaylist = item->parent()->parent(); 426 if ( !m_rootItem->children().contains( collectionOfPlaylist ) ) // verification to make sure we're not stranded 427 { 428 tLog() << "Got what we assumed to be a parent col of a playlist not as a child of our root node...:" << collectionOfPlaylist; 429 return; 430 } 431 432 QModelIndex idx = indexFromItem( collectionOfPlaylist ); 433 m_sourcesWithViewPageItems[ s ] = collectionOfPlaylist; 434 tDebug() << "Emitting dataChanged for offline source:" << idx << idx.isValid() << collectionOfPlaylist << collectionOfPlaylist->text(); 435 emit dataChanged( idx, idx ); 436 } 437 } 438 } 439} 440 441 442SourceTreeItem* 443SourcesModel::activatePlaylistPage( ViewPage* p, SourceTreeItem* i ) 444{ 445 if( !i ) 446 return 0; 447 448 if( qobject_cast< PlaylistItem* >( i ) && 449 qobject_cast< PlaylistItem* >( i )->activateCurrent() ) 450 return i; 451 452 SourceTreeItem* ret = 0; 453 for( int k = 0; k < i->children().size(); k++ ) 454 { 455 if( SourceTreeItem* retItem = activatePlaylistPage( p, i->children().at( k ) ) ) 456 ret = retItem; 457 } 458 459 return ret; 460} 461 462 463void 464SourcesModel::loadSources() 465{ 466 QList<source_ptr> sources = SourceList::instance()->sources(); 467 468 foreach( const source_ptr& source, sources ) 469 appendItem( source ); 470} 471 472 473void 474SourcesModel::onSourcesAdded( const QList<source_ptr>& sources ) 475{ 476 foreach( const source_ptr& source, sources ) 477 appendItem( source ); 478} 479 480 481void 482SourcesModel::onSourceAdded( const source_ptr& source ) 483{ 484 appendItem( source ); 485} 486 487 488void 489SourcesModel::onSourceRemoved( const source_ptr& source ) 490{ 491 removeItem( source ); 492} 493 494 495void 496SourcesModel::itemUpdated() 497{ 498 Q_ASSERT( qobject_cast< SourceTreeItem* >( sender() ) ); 499 SourceTreeItem* item = qobject_cast< SourceTreeItem* >( sender() ); 500 501 if( !item ) 502 return; 503 504 QModelIndex idx = indexFromItem( item ); 505 if( idx.isValid() ) 506 emit dataChanged( idx, idx ); 507} 508 509 510void 511SourcesModel::onItemRowsAddedBegin( int first, int last ) 512{ 513 Q_ASSERT( qobject_cast< SourceTreeItem* >( sender() ) ); 514 SourceTreeItem* item = qobject_cast< SourceTreeItem* >( sender() ); 515 516 if( !item ) 517 return; 518 519 QModelIndex idx = indexFromItem( item ); 520 beginInsertRows( idx, first, last ); 521} 522 523 524void 525SourcesModel::onItemRowsAddedDone() 526{ 527 Q_ASSERT( qobject_cast< SourceTreeItem* >( sender() ) ); 528 529 endInsertRows(); 530} 531 532 533void 534SourcesModel::onItemRowsRemovedBegin( int first, int last ) 535{ 536 Q_ASSERT( qobject_cast< SourceTreeItem* >( sender() ) ); 537 SourceTreeItem* item = qobject_cast< SourceTreeItem* >( sender() ); 538 539 if( !item ) 540 return; 541 542 QModelIndex idx = indexFromItem( item ); 543 beginRemoveRows( idx, first, last ); 544} 545 546 547void 548SourcesModel::onItemRowsRemovedDone() 549{ 550 Q_ASSERT( qobject_cast< SourceTreeItem* >( sender() ) ); 551 552 endRemoveRows(); 553} 554 555 556void 557SourcesModel::linkSourceItemToPage( SourceTreeItem* item, ViewPage* p ) 558{ 559 // TODO handle removal 560 m_sourceTreeLinks[ p ] = item; 561 562 if ( p && m_viewPageDelayedCacheItem == p ) 563 emit selectRequest( QPersistentModelIndex( indexFromItem( item ) ) ); 564 565 if ( QObject* obj = dynamic_cast< QObject* >( p ) ) 566 { 567 if( obj->metaObject()->indexOfSignal( "destroyed(QWidget*)" ) > -1 ) 568 connect( obj, SIGNAL( destroyed( QWidget* ) ), SLOT( onWidgetDestroyed( QWidget* ) ), Qt::UniqueConnection ); 569 } 570 m_viewPageDelayedCacheItem = 0; 571} 572 573 574void 575SourcesModel::onWidgetDestroyed( QWidget* w ) 576{ 577 int ret = m_sourceTreeLinks.remove( dynamic_cast< Tomahawk::ViewPage* > ( w ) ); 578 qDebug() << "REMOVED STALE SOURCE PAGE?" << ret; 579} 580 581 582void 583SourcesModel::removeSourceItemLink( SourceTreeItem* item ) 584{ 585 QList< ViewPage* > pages = m_sourceTreeLinks.keys( item ); 586 foreach( ViewPage* p, pages ) 587 m_sourceTreeLinks.remove( p ); 588} 589 590 591SourceTreeItem* 592SourcesModel::itemFromIndex( const QModelIndex& idx ) const 593{ 594 if( !idx.isValid() ) 595 return m_rootItem; 596 597 Q_ASSERT( idx.internalPointer() ); 598 599 return reinterpret_cast< SourceTreeItem* >( idx.internalPointer() ); 600} 601 602 603QModelIndex 604SourcesModel::indexFromItem( SourceTreeItem* item ) const 605{ 606 if( !item || !item->parent() ) // should never happen.. 607 return QModelIndex(); 608 609 // reconstructs a modelindex from a sourcetreeitem that is somewhere in the tree 610 // traverses the item to the root node, then rebuilds the qmodeindices from there back down 611 // each int is the row of that item in the parent. 612 /** 613 * In this diagram, if the \param item is G, childIndexList will contain [0, 2, 0] 614 * 615 * A 616 * D 617 * E 618 * F 619 * G 620 * H 621 * B 622 * C 623 * 624 **/ 625 QList< int > childIndexList; 626 SourceTreeItem* curItem = item; 627 while( curItem != m_rootItem ) { 628 int row = rowForItem( curItem ); 629 if( row < 0 ) // something went wrong, bail 630 return QModelIndex(); 631 632 childIndexList << row; 633 634 curItem = curItem->parent(); 635 } 636// qDebug() << "build child index list:" << childIndexList; 637 // now rebuild the qmodelindex we need 638 QModelIndex idx; 639 for( int i = childIndexList.size() - 1; i >= 0 ; i-- ) { 640 idx = index( childIndexList[ i ], 0, idx ); 641 } 642// qDebug() << "Got index from item:" << idx << idx.data( Qt::DisplayRole ).toString(); 643// qDebug() << "parent:" << idx.parent(); 644 return idx; 645} 646 647 648int 649SourcesModel::rowForItem( SourceTreeItem* item ) const 650{ 651 if ( !item || !item->parent() || !item->parent()->children().contains( item ) ) 652 return -1; 653 654 return item->parent()->children().indexOf( item ); 655} 656 657 658void 659SourcesModel::itemSelectRequest( SourceTreeItem* item ) 660{ 661 emit selectRequest( QPersistentModelIndex( indexFromItem( item ) ) ); 662} 663 664 665void 666SourcesModel::itemExpandRequest( SourceTreeItem *item ) 667{ 668 emit expandRequest( QPersistentModelIndex( indexFromItem( item ) ) ); 669} 670 671 672void 673SourcesModel::itemToggleExpandRequest( SourceTreeItem *item ) 674{ 675 emit toggleExpandRequest( QPersistentModelIndex( indexFromItem( item ) ) ); 676} 677 678 679QList< source_ptr > 680SourcesModel::sourcesWithViewPage() const 681{ 682 return m_sourcesWithViewPage; 683}