First steps towards making the playlist behave nicely with respect to grouped tracks...
[amarok.git] / src / playlist / PlaylistModel.cpp
blob6f875cf96a38ac179e3312012f0f922b3abc2e0d
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()
56 if( AmarokConfig::savePlaylist() )
58 debug() << "Loading Previous Playlist";
59 m_playlistHandler->load( defaultPlaylistPath() );
61 KActionCollection* ac = Amarok::actionCollection();
62 QAction* undoButton = m_undoStack->createUndoAction( this, i18n("Undo") );
63 undoButton->setIcon( KIcon( Amarok::icon( "undo" ) ) );
64 ac->addAction("playlist_undo", undoButton);
65 QAction* redoButton = m_undoStack->createRedoAction( this, i18n("Redo") );
66 ac->addAction("playlist_redo", redoButton);
67 redoButton->setIcon( KIcon( Amarok::icon( "redo" ) ) );
70 Model::~Model()
72 if( AmarokConfig::savePlaylist() )
74 Meta::TrackList list;
75 foreach( Item* item, itemList() )
77 list << item->track();
79 m_playlistHandler->save( list, defaultPlaylistPath() );
81 delete m_advancer;
82 delete m_playlistHandler;
85 int
86 Model::rowCount( const QModelIndex& ) const
88 return m_items.size();
91 QVariant
92 Model::data( const QModelIndex& index, int role ) const
94 int row = index.row();
95 /*if( ( role == Qt::FontRole) && ( row == m_activeRow ) )
97 QFont original;
98 original.setBold( true );
99 return original;
101 else*/
102 if( role == ItemRole && ( row != -1 ) )
104 return QVariant::fromValue( m_items.at( row ) );
106 else if( role == ActiveTrackRole )
107 return ( row == m_activeRow );
108 else if( role == TrackRole && ( row != -1 ) && m_items.at( row )->track() )
110 return QVariant::fromValue( m_items.at( row )->track() );
112 else if ( role == GroupRole ) {
115 //get the track
116 TrackPtr track = m_items.at( row )->track();
120 if ( !track->album() )
121 return None; // no albm set
122 else if ( !m_albumGroups.contains( track->album() ) )
123 return None; // no group for this album
125 QList< int > rowsInGroup = m_albumGroups.value( track->album() );
127 if ( row == rowsInGroup.first() )
128 return Head;
129 else if ( row == rowsInGroup.last() )
130 return End;
131 else if ( rowsInGroup.contains( row ) )
132 return Body;
133 else
134 return None; //Album has group, but this item is outside it
137 else if( role == Qt::DisplayRole && row != -1 )
139 switch ( index.column() ) {
140 case 0:
141 return m_items.at( row )->track()->name();
142 case 1:
143 return m_items.at( row )->track()->album()->name();
144 case 2:
145 return m_items.at( row )->track()->artist()->name();
148 else
150 return QVariant();
152 /* switch( role )
154 case AlbumArtist: return track->album()->albumArtist()->name();
155 case Album: return track->album()->name();
156 case Artist: return track->artist()->name();
157 case Bitrate: return track->bitrate();
158 case Composer: return track->composer()->name();
159 case CoverImage: return track->album()->image( 50 );
160 case Comment: return track->comment();
161 case DiscNumber: return track->discNumber();
162 case Filesize: return track->filesize();
163 case Genre: return track->genre()->name();
164 case Length: return track->length();
165 case Rating: return track->rating();
166 case Score: return track->score();
167 case Title: return track->name();
168 case TrackNumber: return track->trackNumber();
169 case Year: return track->year()->name().toInt();
170 default: return QVariant();
171 } */
174 // void
175 // insertColumns( int column, Column type )
176 // {
177 // beginInsertColumns( QModelIndex(), column, column + 1 );
178 // m_columns.insert( column, type );
179 // endInsertColumns();
180 // return true;
181 // }
183 void
184 Model::insertTrack( int row, TrackPtr track )
186 DEBUG_BLOCK
187 TrackList list;
188 list.append( track );
189 insertTracks( row, list );
193 void
194 Model::insertTracks( int row, TrackList tracks )
196 m_undoStack->push( new AddTracksCmd( 0, row, tracks ) );
199 bool
200 Model::removeRows( int position, int rows, const QModelIndex& /*parent*/ )
202 m_undoStack->push( new RemoveTracksCmd( 0, position, rows ) );
203 return true;
207 void
208 Model::insertTracks( int row, QueryMaker *qm )
210 qm->startTrackQuery();
211 connect( qm, SIGNAL( queryDone() ), SLOT( queryDone() ) );
212 connect( qm, SIGNAL( newResultReady( QString, Meta::TrackList ) ), SLOT( newResultReady( QString, Meta::TrackList ) ) );
213 m_queryMap.insert( qm, row );
214 qm->run();
217 Qt::DropActions
218 Model::supportedDropActions() const
220 return Qt::CopyAction | Qt::MoveAction;
223 void
224 Model::trackFinished()
226 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
227 return;
228 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
229 track->finishedPlaying( 1.0 ); //TODO: get correct value for parameter
230 m_advancer->advanceTrack();
233 void
234 Model::play( const QModelIndex& index )
236 play( index.row() );
239 void
240 Model::play( int row )
242 setActiveRow( row );
243 EngineController::instance()->play( m_items[ m_activeRow ]->track() );
246 void
247 Model::playlistRepeatMode( int item )
249 if( item == 0 )
250 playModeChanged( Playlist::Standard );
251 else //for now just turn on repeat if anything but "off" is clicked
252 playModeChanged( Playlist::Repeat );
255 void
256 Model::next()
258 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
259 return;
260 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
261 track->finishedPlaying( 0.5 ); //TODO: get correct value for parameter
262 m_advancer->userAdvanceTrack();
265 void
266 Model::back()
268 if( m_activeRow < 0 || m_activeRow >= m_items.size() )
269 return;
270 Meta::TrackPtr track = m_items.at( m_activeRow )->track();
271 track->finishedPlaying( 0.5 ); //TODO: get correct value for parameter
272 m_advancer->recedeTrack();
275 void
276 Model::playCurrentTrack()
278 int selected = m_activeRow;
279 if( selected < 0 || selected >= m_items.size() )
281 //play first track if there are tracks in the playlist
282 if( m_items.size() )
283 selected = 0;
284 else
285 return;
287 play( selected );
291 QString
292 Model::prettyColumnName( Column index ) //static
294 switch( index )
296 case Filename: return i18n( "Filename" );
297 case Title: return i18n( "Title" );
298 case Artist: return i18n( "Artist" );
299 case AlbumArtist:return i18n( "Album Artist");
300 case Composer: return i18n( "Composer" );
301 case Year: return i18n( "Year" );
302 case Album: return i18n( "Album" );
303 case DiscNumber: return i18n( "Disc Number" );
304 case TrackNumber:return i18n( "Track" );
305 case Bpm: return i18n( "BPM" );
306 case Genre: return i18n( "Genre" );
307 case Comment: return i18n( "Comment" );
308 case Directory: return i18n( "Directory" );
309 case Type: return i18n( "Type" );
310 case Length: return i18n( "Length" );
311 case Bitrate: return i18n( "Bitrate" );
312 case SampleRate: return i18n( "Sample Rate" );
313 case Score: return i18n( "Score" );
314 case Rating: return i18n( "Rating" );
315 case PlayCount: return i18n( "Play Count" );
316 case LastPlayed: return i18nc( "Column name", "Last Played" );
317 case Mood: return i18n( "Mood" );
318 case Filesize: return i18n( "File Size" );
319 default: return "This is a bug.";
324 void
325 Model::playModeChanged( int row )
327 delete m_advancer;
328 switch (row)
330 case Playlist::Standard:
331 m_advancer = new StandardTrackNavigator(this);
332 break;
333 case Playlist::Repeat:
334 m_advancer = new RepeatTrackNavigator(this);
335 break;
339 void
340 Model::setActiveRow( int row )
342 DEBUG_BLOCK
344 int max = qMax( row, m_activeRow );
345 int min = qMin( row, m_activeRow );
346 if( ( max - min ) == 1 )
347 emit dataChanged( createIndex( min, 0 ), createIndex( max, 0 ) );
348 else
350 emit dataChanged( createIndex( min, 0 ), createIndex( min, 0 ) );
351 emit dataChanged( createIndex( max, 0 ), createIndex( max, 0 ) );
353 debug() << "between " << min << " and " << max;
354 m_activeRow = row;
357 void
358 Model::metadataChanged( Meta::Track *track )
360 DEBUG_BLOCK
361 const int size = m_items.size();
362 const Meta::TrackPtr needle = Meta::TrackPtr( track );
363 for( int i = 0; i < size; i++ )
365 if( m_items.at( i )->track() == needle )
367 debug() << "Track in playlist";
368 emit dataChanged( createIndex( i, 0 ), createIndex( i, 0 ) );
369 break;
373 #if 0
374 int index = m_tracks.indexOf( Meta::TrackPtr( track ), 0 );
375 if( index != -1 )
376 emit dataChanged( createIndex( index, 0 ), createIndex( index, 0 ) );
377 #endif
380 void
381 Model::metadataChanged(Meta::Album * album)
383 DEBUG_BLOCK
384 //process each track
385 TrackList tracks = album->tracks();
386 foreach( TrackPtr track, tracks ) {
387 metadataChanged( track.data() );
392 void
393 Model::clear()
395 removeRows( 0, m_items.size() );
396 m_albumGroups.clear();
397 m_lastAddedTrackAlbum = AlbumPtr();
398 // m_activeRow = -1;
401 void
402 Model::insertOptioned( Meta::TrackList list, int options )
405 DEBUG_BLOCK
407 //TODO: we call insertOptioned on resume before the statusbar is fully created... We need a better way to handle this
408 // if( list.isEmpty() ) {
409 // Amarok::StatusBar::instance()->shortMessage( i18n("Attempted to insert nothing into playlist.") );
410 // return; // don't add empty items
411 // }
414 if( options & Unique )
416 int alreadyOnPlaylist = 0;
417 for( int i = 0; i < list.size(); ++i )
420 Item* item;
421 foreach( item, m_items )
423 if( item->track() == list.at( i ) )
425 list.removeAt( i );
426 alreadyOnPlaylist++;
427 break;
432 if ( alreadyOnPlaylist )
433 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 ) );
436 int orgCount = rowCount(); //needed because recursion messes up counting
437 bool playlistAdded = false;
439 //HACK! Check if any of the incomming tracks is really a playlist. Warning, this can get highly recursive
440 for( int i = 0; i < list.size(); ++i )
442 if ( m_playlistHandler->isPlaylist( list.at( i )->url() ) ) {
443 playlistAdded = true;
444 m_playlistHandler->load( list.takeAt( i )->url() );
449 int firstItemAdded = -1;
450 if( options & Replace )
452 clear();
453 firstItemAdded = 0;
454 insertTracks( 0, list );
456 else if( options & Append )
458 if ( playlistAdded )
459 firstItemAdded = orgCount;
460 else
461 firstItemAdded = rowCount();
462 insertTracks( firstItemAdded, list );
464 else if( options & Queue )
466 //TODO implement queue
468 if( options & DirectPlay )
470 if ( rowCount() > firstItemAdded )
471 play( firstItemAdded );
473 else if( ( options & StartPlay ) && ( EngineController::engine()->state() != Engine::Playing ) && ( rowCount() != 0 ) )
475 play( firstItemAdded );
477 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
480 void
481 Model::insertOptioned( Meta::TrackPtr track, int options )
483 Meta::TrackList list;
484 list.append( track );
485 insertOptioned( list, options );
488 void
489 Model::insertOptioned( QueryMaker *qm, int options )
491 qm->startTrackQuery();
492 connect( qm, SIGNAL( queryDone() ), SLOT( queryDone() ) );
493 connect( qm, SIGNAL( newResultReady( QString, Meta::TrackList ) ), SLOT( newResultReady( QString, Meta::TrackList ) ) );
494 m_optionedQueryMap.insert( qm, options );
495 qm->run();
498 void
499 Model::insertMedia( KUrl::List list, int options )
501 KUrl url;
502 Meta::TrackList trackList;
503 foreach( url, list )
505 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
506 if( track )
507 trackList.push_back( track );
509 if( trackList.isEmpty() )
510 debug() << "Attempted to insert nothing into the playlist!";
512 insertOptioned( trackList, options );
515 bool
516 Model::saveM3U( const QString &path ) const
518 Meta::TrackList tl;
519 foreach( Item* item, itemList() )
520 tl << item->track();
521 m_playlistHandler->save( tl, path );
524 Qt::ItemFlags
525 Model::flags(const QModelIndex &index) const
527 if( index.isValid() )
529 return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled |
530 Qt::ItemIsDragEnabled | Qt::ItemIsSelectable );
532 return Qt::ItemIsDropEnabled;
535 QStringList
536 Model::mimeTypes() const //reimplemented
538 QStringList ret = QAbstractListModel::mimeTypes();
539 ret << AmarokMimeData::TRACK_MIME;
540 return ret;
543 QMimeData*
544 Model::mimeData( const QModelIndexList &indexes ) const //reimplemented
546 AmarokMimeData* mime = new AmarokMimeData();
547 Meta::TrackList selectedTracks;
549 foreach( QModelIndex it, indexes )
550 selectedTracks << m_items.at( it.row() )->track();
552 mime->setTracks( selectedTracks );
553 return mime;
556 bool
557 Model::dropMimeData ( const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent ) //reimplemented
559 Q_UNUSED( column ); Q_UNUSED( parent );
560 DEBUG_BLOCK
562 if( action == Qt::IgnoreAction )
563 return true;
565 if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
567 debug() << "Found track mime type";
569 const AmarokMimeData* trackListDrag = dynamic_cast<const AmarokMimeData*>( data );
570 if( trackListDrag )
572 if( row < 0 )
574 debug() << "Inserting at row: " << row << " so we're appending to the list.";
575 insertOptioned( trackListDrag->tracks(), Playlist::Append );
577 else
579 debug() << "Inserting at row: " << row <<" so its inserted correctly.";
580 insertTracks( row, trackListDrag->tracks() );
582 return true;
585 else if( data->hasUrls() )
587 //probably a drop from an external source
588 debug() << "Drop from external source";
589 QList<QUrl> urls = data->urls();
590 Meta::TrackList tracks;
591 foreach( QUrl url, urls )
593 Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( KUrl( url ) );
594 if( track )
595 tracks.append( track );
597 if( !tracks.isEmpty() )
598 insertOptioned( tracks, Playlist::Append );
599 return true;
601 return false;
604 ////////////
605 //Private Methods
606 ///////////
609 void
610 Model::insertTracksCommand( int row, TrackList list )
612 DEBUG_BLOCK
613 debug() << "inserting... " << row << ' ' << list.count();
614 if( !list.size() )
615 return;
617 beginInsertRows( QModelIndex(), row, row + list.size() - 1 );
618 int i = 0;
619 foreach( TrackPtr track , list )
621 if( track )
623 track->subscribe( this );
624 if( track->album() ) {
625 track->album()->subscribe( this );
627 //check if track should be added to an album group or
628 //if a new album group should be created
630 if ( track->album() == m_lastAddedTrackAlbum ) {
631 //this thrack and the last one should be grouped
633 //does a group already exist?
634 if ( m_albumGroups.contains( track->album() ) ) {
635 //yes, add track to existing album group
636 m_albumGroups[track->album()].append( row + i );
637 } else {
638 //no group exists... create new one and add this and last track
639 m_albumGroups.insert( track->album(), QList < int > () );
640 m_albumGroups[track->album()].append( row + i -1 );
641 m_albumGroups[track->album()].append( row + i );
645 m_lastAddedTrackAlbum = track->album();
647 } else {
648 //make sure we dont group tracks "around" tracks with no album set
649 m_lastAddedTrackAlbum = AlbumPtr();
652 m_items.insert( row + i, new Item( track ) );
653 i++;
656 endInsertRows();
657 //push up the active row if needed
658 if( m_activeRow > row )
660 int oldActiveRow = m_activeRow;
661 m_activeRow += list.size();
662 Q_UNUSED( oldActiveRow );
663 //dataChanged( createIndex( oldActiveRow, 0 ), createIndex( oldActiveRow, columnCount() -1 ) );
664 //dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() -1 ) );
666 dataChanged( createIndex( row, 0 ), createIndex( rowCount() - 1, 0 ) );
667 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
669 emit playlistCountChanged( rowCount() );
673 TrackList
674 Model::removeRowsCommand( int position, int rows )
676 DEBUG_BLOCK
678 beginRemoveRows( QModelIndex(), position, position + rows - 1 );
679 // TrackList::iterator start = m_tracks.begin() + position;
680 // TrackList::iterator end = start + rows;
681 // m_tracks.erase( start, end );
682 TrackList ret;
683 for( int i = position; i < position + rows; i++ )
685 Item* item = m_items.takeAt( position ); //take at position, row times
686 item->track()->unsubscribe( this );
687 if( item->track()->album() )
688 item->track()->album()->unsubscribe( this );
689 ret.push_back( item->track() );
690 delete item;
692 endRemoveRows();
694 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items.isEmpty() );
696 //update m_activeRow
697 bool activeRowChanged = true;
698 bool oldActiveRow = m_activeRow;
699 if( m_activeRow >= position && m_activeRow < ( position + rows ) )
700 m_activeRow = -1;
701 else if( m_activeRow >= position )
702 m_activeRow = m_activeRow - position;
703 else
704 activeRowChanged = false;
705 if( activeRowChanged )
707 dataChanged( createIndex( oldActiveRow, 0 ), createIndex( oldActiveRow, columnCount() -1 ) );
708 dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() -1 ) );
710 dataChanged( createIndex( position, 0 ), createIndex( rowCount(), 0 ) );
713 regroupAlbums();
715 emit playlistCountChanged( rowCount() );
716 return ret;
719 void
720 Model::queryDone() //Slot
722 QueryMaker *qm = dynamic_cast<QueryMaker*>( sender() );
723 if( qm )
725 m_queryMap.remove( qm );
726 qm->deleteLater();
730 void
731 Model::newResultReady( const QString &collectionId, const Meta::TrackList &tracks ) //Slot
733 Q_UNUSED( collectionId )
734 QueryMaker *qm = dynamic_cast<QueryMaker*>( sender() );
735 if( qm )
737 //requires better handling of queries which return multiple results
738 if( m_queryMap.contains( qm ) )
739 insertTracks( m_queryMap.value( qm ), tracks );
740 else if( m_optionedQueryMap.contains( qm ) )
741 insertOptioned( tracks, m_optionedQueryMap.value( qm ) );
745 QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
748 Q_UNUSED( orientation );
750 if ( role != Qt::DisplayRole )
751 return QVariant();
753 switch ( section )
755 case 0:
756 return "title";
757 case 1:
758 return "album";
759 case 2:
760 return "artist";
761 default:
762 return QVariant();
768 void Model::moveRow(int row, int to)
771 m_items.move( row, to );
773 regroupAlbums();
778 void Model::regroupAlbums()
781 m_albumGroups.clear();
782 m_lastAddedTrackAlbum = AlbumPtr();
784 TrackPtr lastTrack;
786 debug() << "number of rows: " << m_items.count();
788 int i;
789 for ( i = 0; i < m_items.count(); i++ )
792 debug() << "i: " << i;
793 TrackPtr track = m_items.at( i )->track();
795 //is this track in the same album as the one preceding it?
796 if ( lastTrack.data() != 0 ) {
797 if ( lastTrack->album() == track->album() ) {
798 //Group 'em!
800 //does this album already have a group entry?
801 if ( m_albumGroups.contains( track->album() ) ) {
802 //add track to existing album group
803 m_albumGroups[track->album()].append( i );
804 } else {
805 //no group exists... create new one and add this and last track
806 m_albumGroups.insert( track->album(), QList < int > () );
807 m_albumGroups[track->album()].append( i -1 );
808 m_albumGroups[track->album()].append( i );
815 lastTrack = track;
819 //reset();
824 namespace The {
825 Playlist::Model* playlistModel() { return Playlist::Model::s_instance; }
835 #include "PlaylistModel.moc"