Add basic support for collapsing album groups. Not currently used.
[amarok.git] / src / playlist / PlaylistModel.cpp
blob88c71042c6d2670099fbb55a9dcafbc9a4e38631
1 /***************************************************************************
2 * copyright : (C) 2007 Ian Monroe <ian@monroe.nu> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License version 2 *
6 * as published by the Free Software Foundation. *
7 ***************************************************************************/
9 #include "PlaylistModel.h"
11 #include "amarok.h"
12 #include "amarokconfig.h"
13 #include "AmarokMimeData.h"
14 #include "debug.h"
15 #include "enginecontroller.h"
16 #include "PlaylistItem.h"
17 #include "RepeatTrackNavigator.h"
18 #include "StandardTrackNavigator.h"
19 #include "statusbar.h"
20 #include "TheInstances.h"
21 #include "UndoCommands.h"
23 #include "collection/BlockingQuery.h"
24 #include "collection/Collection.h"
25 #include "collection/CollectionManager.h"
26 #include "collection/QueryMaker.h"
27 #include "meta/lastfm/LastFmMeta.h"
29 #include <QAction>
30 #include <QStringList>
31 #include <QUndoStack>
33 #include <KIcon>
34 #include <KUrl>
36 using namespace Playlist;
37 using namespace Meta;
39 Model *Model::s_instance = 0;
41 Model::Model( QObject* parent )
42 : QAbstractListModel( parent )
43 , m_activeRow( -1 )
44 , m_advancer( new StandardTrackNavigator( this ) )
45 , m_undoStack( new QUndoStack( this ) )
46 , m_playlistHandler ( new PlaylistHandler )
48 connect( EngineController::instance(), SIGNAL( trackFinished() ), this, SLOT( trackFinished() ) );
49 connect( EngineController::instance(), SIGNAL( orderCurrent() ), this, SLOT( playCurrentTrack() ) );
50 s_instance = this;
53 void
54 Model::init()
57 KActionCollection* ac = Amarok::actionCollection();
58 QAction* undoButton = m_undoStack->createUndoAction( this, i18n("Undo") );
59 undoButton->setIcon( KIcon( Amarok::icon( "undo" ) ) );
60 ac->addAction("playlist_undo", undoButton);
61 QAction* redoButton = m_undoStack->createRedoAction( this, i18n("Redo") );
62 ac->addAction("playlist_redo", redoButton);
63 redoButton->setIcon( KIcon( Amarok::icon( "redo" ) ) );
66 Model::~Model()
68 if( AmarokConfig::savePlaylist() )
70 Meta::TrackList list;
71 foreach( Item* item, itemList() )
73 list << item->track();
75 m_playlistHandler->save( list, defaultPlaylistPath() );
77 delete m_advancer;
78 delete m_playlistHandler;
81 int
82 Model::rowCount( const QModelIndex& ) const
84 return m_items.size();
87 QVariant
88 Model::data( const QModelIndex& index, int role ) const
90 int row = index.row();
91 /*if( ( role == Qt::FontRole) && ( row == m_activeRow ) )
93 QFont original;
94 original.setBold( true );
95 return original;
97 else*/
98 if( role == ItemRole && ( row != -1 ) )
100 return QVariant::fromValue( m_items.at( row ) );
102 else if( role == ActiveTrackRole )
103 return ( row == m_activeRow );
104 else if( role == TrackRole && ( row != -1 ) && m_items.at( row )->track() )
106 return QVariant::fromValue( m_items.at( row )->track() );
108 else if ( role == GroupRole ) {
111 //get the track
112 TrackPtr track = m_items.at( row )->track();
114 if ( !track->album() )
115 return None; // no albm set
116 else if ( !m_albumGroups.contains( track->album() ) )
117 return None; // no group for this album, should never happen...
119 AlbumGroup albumGroup = m_albumGroups.value( track->album() );
120 return albumGroup.groupMode( row );
122 } else if ( role == GroupedTracksRole ) {
123 //get the track
124 TrackPtr track = m_items.at( row )->track();
126 AlbumGroup albumGroup = m_albumGroups.value( track->album() );
127 return albumGroup.elementsInGroup( row );
129 } else if( role == Qt::DisplayRole && row != -1 )
131 switch ( index.column() ) {
132 case 0:
133 return m_items.at( row )->track()->name();
134 case 1:
135 if ( m_items.at( row )->track()->album() )
136 return m_items.at( row )->track()->album()->name();
137 else
138 return "";
139 case 2:
140 if ( m_items.at( row )->track()->artist() )
141 return m_items.at( row )->track()->artist()->name();
142 else
143 return "";
146 else
148 return QVariant();
150 /* switch( role )
152 case AlbumArtist: return track->album()->albumArtist()->name();
153 case Album: return track->album()->name();
154 case Artist: return track->artist()->name();
155 case Bitrate: return track->bitrate();
156 case Composer: return track->composer()->name();
157 case CoverImage: return track->album()->image( 50 );
158 case Comment: return track->comment();
159 case DiscNumber: return track->discNumber();
160 case Filesize: return track->filesize();
161 case Genre: return track->genre()->name();
162 case Length: return track->length();
163 case Rating: return track->rating();
164 case Score: return track->score();
165 case Title: return track->name();
166 case TrackNumber: return track->trackNumber();
167 case Year: return track->year()->name().toInt();
168 default: return QVariant();
169 } */
172 // void
173 // insertColumns( int column, Column type )
174 // {
175 // beginInsertColumns( QModelIndex(), column, column + 1 );
176 // m_columns.insert( column, type );
177 // endInsertColumns();
178 // return true;
179 // }
181 void
182 Model::insertTrack( int row, TrackPtr track )
184 DEBUG_BLOCK
185 TrackList list;
186 list.append( track );
187 insertTracks( row, list );
191 void
192 Model::insertTracks( int row, TrackList tracks )
194 m_undoStack->push( new AddTracksCmd( 0, row, tracks ) );
197 bool
198 Model::removeRows( int position, int rows, const QModelIndex& /*parent*/ )
200 m_undoStack->push( new RemoveTracksCmd( 0, position, rows ) );
201 return true;
205 void
206 Model::insertTracks( int row, QueryMaker *qm )
208 qm->startTrackQuery();
209 connect( qm, SIGNAL( queryDone() ), SLOT( queryDone() ) );
210 connect( qm, SIGNAL( newResultReady( QString, Meta::TrackList ) ), SLOT( newResultReady( QString, Meta::TrackList ) ) );
211 m_queryMap.insert( qm, row );
212 qm->run();
215 Qt::DropActions
216 Model::supportedDropActions() const
218 return Qt::CopyAction | Qt::MoveAction;
221 void
222 Model::trackFinished()
224 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
225 return;
226 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
227 track->finishedPlaying( 1.0 ); //TODO: get correct value for parameter
228 m_advancer->advanceTrack();
231 void
232 Model::play( const QModelIndex& index )
234 play( index.row() );
237 void
238 Model::play( int row )
241 //make sure that a group containg this track is expanded
242 if ( m_albumGroups.contains( m_items[ row ]->track()->album() ) ) {
243 m_albumGroups[ m_items[ row ]->track()->album() ].setCollapsed( row, false );
244 debug() << "Here";
248 setActiveRow( row );
249 EngineController::instance()->play( m_items[ m_activeRow ]->track() );
252 void
253 Model::playlistRepeatMode( int item )
255 if( item == 0 )
256 playModeChanged( Playlist::Standard );
257 else //for now just turn on repeat if anything but "off" is clicked
258 playModeChanged( Playlist::Repeat );
261 void
262 Model::next()
264 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
265 return;
266 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
267 track->finishedPlaying( 0.5 ); //TODO: get correct value for parameter
268 m_advancer->userAdvanceTrack();
271 void
272 Model::back()
274 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
275 return;
276 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
277 track->finishedPlaying( 0.5 ); //TODO: get correct value for parameter
278 m_advancer->recedeTrack();
281 void
282 Model::playCurrentTrack()
284 int selected = m_activeRow;
285 if( selected < 0 || selected >= m_items.size() )
287 //play first track if there are tracks in the playlist
288 if( m_items.size() )
289 selected = 0;
290 else
291 return;
293 play( selected );
297 QString
298 Model::prettyColumnName( Column index ) //static
300 switch( index )
302 case Filename: return i18n( "Filename" );
303 case Title: return i18n( "Title" );
304 case Artist: return i18n( "Artist" );
305 case AlbumArtist:return i18n( "Album Artist");
306 case Composer: return i18n( "Composer" );
307 case Year: return i18n( "Year" );
308 case Album: return i18n( "Album" );
309 case DiscNumber: return i18n( "Disc Number" );
310 case TrackNumber:return i18n( "Track" );
311 case Bpm: return i18n( "BPM" );
312 case Genre: return i18n( "Genre" );
313 case Comment: return i18n( "Comment" );
314 case Directory: return i18n( "Directory" );
315 case Type: return i18n( "Type" );
316 case Length: return i18n( "Length" );
317 case Bitrate: return i18n( "Bitrate" );
318 case SampleRate: return i18n( "Sample Rate" );
319 case Score: return i18n( "Score" );
320 case Rating: return i18n( "Rating" );
321 case PlayCount: return i18n( "Play Count" );
322 case LastPlayed: return i18nc( "Column name", "Last Played" );
323 case Mood: return i18n( "Mood" );
324 case Filesize: return i18n( "File Size" );
325 default: return "This is a bug.";
330 void
331 Model::playModeChanged( int row )
333 delete m_advancer;
334 switch (row)
336 case Playlist::Standard:
337 m_advancer = new StandardTrackNavigator(this);
338 break;
339 case Playlist::Repeat:
340 m_advancer = new RepeatTrackNavigator(this);
341 break;
345 void
346 Model::setActiveRow( int row )
348 DEBUG_BLOCK
350 int max = qMax( row, m_activeRow );
351 int min = qMin( row, m_activeRow );
352 if( ( max - min ) == 1 )
353 emit dataChanged( createIndex( min, 0 ), createIndex( max, 0 ) );
354 else
356 emit dataChanged( createIndex( min, 0 ), createIndex( min, 0 ) );
357 emit dataChanged( createIndex( max, 0 ), createIndex( max, 0 ) );
359 debug() << "between " << min << " and " << max;
360 m_activeRow = row;
363 void
364 Model::metadataChanged( Meta::Track *track )
366 DEBUG_BLOCK
367 const int size = m_items.size();
368 const Meta::TrackPtr needle = Meta::TrackPtr( track );
369 for( int i = 0; i < size; i++ )
371 if( m_items.at( i )->track() == needle )
373 debug() << "Track in playlist";
374 emit dataChanged( createIndex( i, 0 ), createIndex( i, 0 ) );
375 break;
379 #if 0
380 int index = m_tracks.indexOf( Meta::TrackPtr( track ), 0 );
381 if( index != -1 )
382 emit dataChanged( createIndex( index, 0 ), createIndex( index, 0 ) );
383 #endif
386 void
387 Model::metadataChanged(Meta::Album * album)
389 DEBUG_BLOCK
390 //process each track
391 TrackList tracks = album->tracks();
392 foreach( TrackPtr track, tracks ) {
393 metadataChanged( track.data() );
398 void
399 Model::clear()
401 if( m_items.size() < 1 )
402 return;
403 removeRows( 0, m_items.size() );
404 m_albumGroups.clear();
405 m_lastAddedTrackAlbum = AlbumPtr();
406 // m_activeRow = -1;
409 void
410 Model::insertOptioned( Meta::TrackList list, int options )
413 DEBUG_BLOCK
415 //TODO: we call insertOptioned on resume before the statusbar is fully created... We need a better way to handle this
416 // if( list.isEmpty() ) {
417 // Amarok::StatusBar::instance()->shortMessage( i18n("Attempted to insert nothing into playlist.") );
418 // return; // don't add empty items
419 // }
422 if( options & Unique )
424 int alreadyOnPlaylist = 0;
425 for( int i = 0; i < list.size(); ++i )
428 Item* item;
429 foreach( item, m_items )
431 if( item->track() == list.at( i ) )
433 list.removeAt( i );
434 alreadyOnPlaylist++;
435 break;
440 if ( alreadyOnPlaylist )
441 Amarok::StatusBar::instance()->shortMessage( i18np("One track was already in the playlist, so it was not added.", "%1 tracks were already in the playlist, so they were not added.", alreadyOnPlaylist ) );
444 int orgCount = rowCount(); //needed because recursion messes up counting
445 bool playlistAdded = false;
447 //HACK! Check if any of the incomming tracks is really a playlist. Warning, this can get highly recursive
448 for( int i = 0; i < list.size(); ++i )
450 if ( m_playlistHandler->isPlaylist( list.at( i )->url() ) ) {
451 playlistAdded = true;
452 m_playlistHandler->load( list.takeAt( i )->url() );
457 int firstItemAdded = -1;
458 if( options & Replace )
460 clear();
461 firstItemAdded = 0;
462 insertTracks( 0, list );
464 else if( options & Append )
466 if ( playlistAdded )
467 firstItemAdded = orgCount;
468 else
469 firstItemAdded = rowCount();
470 insertTracks( firstItemAdded, list );
471 if( orgCount == 0 && (EngineController::engine()->state() != Engine::Playing ) )
472 play( firstItemAdded );
474 else if( options & Queue )
476 //TODO implement queue
478 if( options & DirectPlay )
480 if ( rowCount() > firstItemAdded )
481 play( firstItemAdded );
483 else if( ( options & StartPlay ) && ( EngineController::engine()->state() != Engine::Playing ) && ( rowCount() != 0 ) )
485 play( firstItemAdded );
487 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
490 void
491 Model::insertOptioned( Meta::TrackPtr track, int options )
493 Meta::TrackList list;
494 list.append( track );
495 insertOptioned( list, options );
498 void
499 Model::insertOptioned( QueryMaker *qm, int options )
501 qm->startTrackQuery();
502 connect( qm, SIGNAL( queryDone() ), SLOT( queryDone() ) );
503 connect( qm, SIGNAL( newResultReady( QString, Meta::TrackList ) ), SLOT( newResultReady( QString, Meta::TrackList ) ) );
504 m_optionedQueryMap.insert( qm, options );
505 qm->run();
508 void
509 Model::insertMedia( KUrl::List list, int options )
511 KUrl url;
512 Meta::TrackList trackList;
513 foreach( url, list )
515 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
516 if( track )
517 trackList.push_back( track );
519 if( trackList.isEmpty() )
520 debug() << "Attempted to insert nothing into the playlist!";
522 insertOptioned( trackList, options );
525 bool
526 Model::saveM3U( const QString &path ) const
528 Meta::TrackList tl;
529 foreach( Item* item, itemList() )
530 tl << item->track();
531 if( m_playlistHandler->save( tl, path ) )
532 return true;
535 Qt::ItemFlags
536 Model::flags(const QModelIndex &index) const
538 if( index.isValid() )
540 return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled |
541 Qt::ItemIsDragEnabled | Qt::ItemIsSelectable );
543 return Qt::ItemIsDropEnabled;
546 QStringList
547 Model::mimeTypes() const //reimplemented
549 QStringList ret = QAbstractListModel::mimeTypes();
550 ret << AmarokMimeData::TRACK_MIME;
551 return ret;
554 QMimeData*
555 Model::mimeData( const QModelIndexList &indexes ) const //reimplemented
557 AmarokMimeData* mime = new AmarokMimeData();
558 Meta::TrackList selectedTracks;
560 foreach( QModelIndex it, indexes )
561 selectedTracks << m_items.at( it.row() )->track();
563 mime->setTracks( selectedTracks );
564 return mime;
567 bool
568 Model::dropMimeData ( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) //reimplemented
570 Q_UNUSED( column ); Q_UNUSED( parent );
571 DEBUG_BLOCK
573 if( action == Qt::IgnoreAction )
574 return true;
576 if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
578 debug() << "Found track mime type";
580 const AmarokMimeData* trackListDrag = dynamic_cast<const AmarokMimeData*>( data );
581 if( trackListDrag )
583 if( row < 0 )
585 debug() << "Inserting at row: " << row << " so we're appending to the list.";
586 insertOptioned( trackListDrag->tracks(), Playlist::Append );
588 else
590 debug() << "Inserting at row: " << row <<" so its inserted correctly.";
591 insertTracks( row, trackListDrag->tracks() );
593 return true;
596 else if( data->hasUrls() )
598 //probably a drop from an external source
599 debug() << "Drop from external source";
600 QList<QUrl> urls = data->urls();
601 Meta::TrackList tracks;
602 foreach( QUrl url, urls )
604 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( KUrl( url ) );
605 if( track )
606 tracks.append( track );
608 if( !tracks.isEmpty() )
609 insertOptioned( tracks, Playlist::Append );
610 return true;
612 return false;
615 ////////////
616 //Private Methods
617 ///////////
620 void
621 Model::insertTracksCommand( int row, TrackList list )
623 DEBUG_BLOCK
624 debug() << "inserting... " << row << ' ' << list.count();
625 if( !list.size() )
626 return;
628 beginInsertRows( QModelIndex(), row, row + list.size() - 1 );
629 int i = 0;
630 foreach( TrackPtr track , list )
632 if( track )
634 track->subscribe( this );
635 if( track->album() )
636 track->album()->subscribe( this );
638 m_items.insert( row + i, new Item( track ) );
639 i++;
643 regroupAlbums();
645 endInsertRows();
646 //push up the active row if needed
647 if( m_activeRow > row )
649 int oldActiveRow = m_activeRow;
650 m_activeRow += list.size();
651 Q_UNUSED( oldActiveRow );
652 //dataChanged( createIndex( oldActiveRow, 0 ), createIndex( oldActiveRow, columnCount() -1 ) );
653 //dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() -1 ) );
655 dataChanged( createIndex( row, 0 ), createIndex( rowCount() - 1, 0 ) );
656 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
659 emit playlistCountChanged( rowCount() );
663 TrackList
664 Model::removeRowsCommand( int position, int rows )
666 DEBUG_BLOCK
668 beginRemoveRows( QModelIndex(), position, position + rows - 1 );
669 // TrackList::iterator start = m_tracks.begin() + position;
670 // TrackList::iterator end = start + rows;
671 // m_tracks.erase( start, end );
672 TrackList ret;
673 for( int i = position; i < position + rows; i++ )
675 Item* item = m_items.takeAt( position ); //take at position, row times
676 item->track()->unsubscribe( this );
677 if( item->track()->album() )
678 item->track()->album()->unsubscribe( this );
679 ret.push_back( item->track() );
680 delete item;
682 endRemoveRows();
684 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
686 //update m_activeRow
687 bool activeRowChanged = true;
688 bool oldActiveRow = m_activeRow;
689 if( m_activeRow >= position && m_activeRow < ( position + rows ) )
690 m_activeRow = -1;
691 else if( m_activeRow >= position )
692 m_activeRow = m_activeRow - position;
693 else
694 activeRowChanged = false;
695 if( activeRowChanged )
697 dataChanged( createIndex( oldActiveRow, 0 ), createIndex( oldActiveRow, columnCount() -1 ) );
698 dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() -1 ) );
700 dataChanged( createIndex( position, 0 ), createIndex( rowCount(), 0 ) );
702 regroupAlbums();
704 emit playlistCountChanged( rowCount() );
705 return ret;
708 void
709 Model::queryDone() //Slot
711 QueryMaker *qm = dynamic_cast<QueryMaker*>( sender() );
712 if( qm )
714 m_queryMap.remove( qm );
715 qm->deleteLater();
719 void
720 Model::newResultReady( const QString &collectionId, const Meta::TrackList &tracks ) //Slot
722 Q_UNUSED( collectionId )
723 QueryMaker *qm = dynamic_cast<QueryMaker*>( sender() );
724 if( qm )
726 //requires better handling of queries which return multiple results
727 if( m_queryMap.contains( qm ) )
728 insertTracks( m_queryMap.value( qm ), tracks );
729 else if( m_optionedQueryMap.contains( qm ) )
730 insertOptioned( tracks, m_optionedQueryMap.value( qm ) );
734 QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
737 Q_UNUSED( orientation );
739 if ( role != Qt::DisplayRole )
740 return QVariant();
742 switch ( section )
744 case 0:
745 return "title";
746 case 1:
747 return "album";
748 case 2:
749 return "artist";
750 default:
751 return QVariant();
757 void Model::moveRow(int row, int to)
760 m_items.move( row, to );
762 regroupAlbums();
767 void Model::regroupAlbums()
769 DEBUG_BLOCK
771 m_albumGroups.clear();
772 //m_lastAddedTrackAlbum = AlbumPtr();
774 //TrackPtr lastTrack;
776 debug() << "number of rows: " << m_items.count();
778 int i;
779 for ( i = 0; i < m_items.count(); i++ )
784 debug() << "i: " << i;
787 TrackPtr track = m_items.at( i )->track();
789 if ( !track->album() )
790 continue;
792 if ( m_albumGroups.contains( track->album() ) ) {
793 m_albumGroups[ track->album() ].addRow( i );
794 } else {
795 AlbumGroup newGroup;
796 newGroup.addRow( i );
797 m_albumGroups.insert( track->album(), newGroup );
803 //make sure that a group containg playing track is expanded
804 if ( m_activeRow != -1 ){
805 if ( m_albumGroups.contains( m_items[ m_activeRow ]->track()->album() ) ) {
806 m_albumGroups[ m_items[ m_activeRow ]->track()->album() ].setCollapsed( m_activeRow, false );
807 debug() << "Here";
811 //reset();
816 namespace The {
817 Playlist::Model* playlistModel() { return Playlist::Model::s_instance; }
827 #include "PlaylistModel.moc"