1 /***************************************************************************
2 * copyright : (C) 2007 Ian Monroe <ian@monroe.nu> *
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"
12 #include "amarokconfig.h"
13 #include "AmarokMimeData.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"
30 #include <QStringList>
36 using namespace Playlist
;
39 Model
*Model::s_instance
= 0;
41 Model::Model( QObject
* parent
)
42 : QAbstractListModel( parent
)
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() ) );
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" ) ) );
72 if( AmarokConfig::savePlaylist() )
75 foreach( Item
* item
, itemList() )
77 list
<< item
->track();
79 m_playlistHandler
->save( list
, defaultPlaylistPath() );
82 delete m_playlistHandler
;
86 Model::rowCount( const QModelIndex
& ) const
88 return m_items
.size();
92 Model::data( const QModelIndex
& index
, int role
) const
94 int row
= index
.row();
95 /*if( ( role == Qt::FontRole) && ( row == m_activeRow ) )
98 original.setBold( true );
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
) {
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() )
129 else if ( row
== rowsInGroup
.last() )
131 else if ( rowsInGroup
.contains( row
) )
134 return None
; //Album has group, but this item is outside it
137 else if( role
== Qt::DisplayRole
&& row
!= -1 )
139 switch ( index
.column() ) {
141 return m_items
.at( row
)->track()->name();
143 return m_items
.at( row
)->track()->album()->name();
145 return m_items
.at( row
)->track()->artist()->name();
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();
175 // insertColumns( int column, Column type )
177 // beginInsertColumns( QModelIndex(), column, column + 1 );
178 // m_columns.insert( column, type );
179 // endInsertColumns();
184 Model::insertTrack( int row
, TrackPtr track
)
188 list
.append( track
);
189 insertTracks( row
, list
);
194 Model::insertTracks( int row
, TrackList tracks
)
196 m_undoStack
->push( new AddTracksCmd( 0, row
, tracks
) );
200 Model::removeRows( int position
, int rows
, const QModelIndex
& /*parent*/ )
202 m_undoStack
->push( new RemoveTracksCmd( 0, position
, rows
) );
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
);
218 Model::supportedDropActions() const
220 return Qt::CopyAction
| Qt::MoveAction
;
224 Model::trackFinished()
226 if( m_activeRow
< 0 || m_activeRow
>= m_items
.size() )
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();
234 Model::play( const QModelIndex
& index
)
240 Model::play( int row
)
243 EngineController::instance()->play( m_items
[ m_activeRow
]->track() );
247 Model::playlistRepeatMode( int item
)
250 playModeChanged( Playlist::Standard
);
251 else //for now just turn on repeat if anything but "off" is clicked
252 playModeChanged( Playlist::Repeat
);
258 if( m_activeRow
< 0 || m_activeRow
>= m_items
.size() )
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();
268 if( m_activeRow
< 0 || m_activeRow
>= m_items
.size() )
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();
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
292 Model::prettyColumnName( Column index
) //static
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.";
325 Model::playModeChanged( int row
)
330 case Playlist::Standard
:
331 m_advancer
= new StandardTrackNavigator(this);
333 case Playlist::Repeat
:
334 m_advancer
= new RepeatTrackNavigator(this);
340 Model::setActiveRow( int row
)
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 ) );
350 emit
dataChanged( createIndex( min
, 0 ), createIndex( min
, 0 ) );
351 emit
dataChanged( createIndex( max
, 0 ), createIndex( max
, 0 ) );
353 debug() << "between " << min
<< " and " << max
;
358 Model::metadataChanged( Meta::Track
*track
)
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 ) );
374 int index
= m_tracks
.indexOf( Meta::TrackPtr( track
), 0 );
376 emit
dataChanged( createIndex( index
, 0 ), createIndex( index
, 0 ) );
381 Model::metadataChanged(Meta::Album
* album
)
385 TrackList tracks
= album
->tracks();
386 foreach( TrackPtr track
, tracks
) {
387 metadataChanged( track
.data() );
395 removeRows( 0, m_items
.size() );
396 m_albumGroups
.clear();
397 m_lastAddedTrackAlbum
= AlbumPtr();
402 Model::insertOptioned( Meta::TrackList list
, int options
)
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
414 if( options
& Unique
)
416 int alreadyOnPlaylist
= 0;
417 for( int i
= 0; i
< list
.size(); ++i
)
421 foreach( item
, m_items
)
423 if( item
->track() == list
.at( i
) )
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
)
454 insertTracks( 0, list
);
456 else if( options
& Append
)
459 firstItemAdded
= orgCount
;
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() );
481 Model::insertOptioned( Meta::TrackPtr track
, int options
)
483 Meta::TrackList list
;
484 list
.append( track
);
485 insertOptioned( list
, options
);
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
);
499 Model::insertMedia( KUrl::List list
, int options
)
502 Meta::TrackList trackList
;
505 Meta::TrackPtr track
= CollectionManager::instance()->trackForUrl( url
);
507 trackList
.push_back( track
);
509 if( trackList
.isEmpty() )
510 debug() << "Attempted to insert nothing into the playlist!";
512 insertOptioned( trackList
, options
);
516 Model::saveM3U( const QString
&path
) const
519 foreach( Item
* item
, itemList() )
521 m_playlistHandler
->save( tl
, path
);
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
;
536 Model::mimeTypes() const //reimplemented
538 QStringList ret
= QAbstractListModel::mimeTypes();
539 ret
<< AmarokMimeData::TRACK_MIME
;
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
);
557 Model::dropMimeData ( const QMimeData
* data
, Qt::DropAction action
, int row
, int column
, const QModelIndex
& parent
) //reimplemented
559 Q_UNUSED( column
); Q_UNUSED( parent
);
562 if( action
== Qt::IgnoreAction
)
565 if( data
->hasFormat( AmarokMimeData::TRACK_MIME
) )
567 debug() << "Found track mime type";
569 const AmarokMimeData
* trackListDrag
= dynamic_cast<const AmarokMimeData
*>( data
);
574 debug() << "Inserting at row: " << row
<< " so we're appending to the list.";
575 insertOptioned( trackListDrag
->tracks(), Playlist::Append
);
579 debug() << "Inserting at row: " << row
<<" so its inserted correctly.";
580 insertTracks( row
, trackListDrag
->tracks() );
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
) );
595 tracks
.append( track
);
597 if( !tracks
.isEmpty() )
598 insertOptioned( tracks
, Playlist::Append
);
610 Model::insertTracksCommand( int row
, TrackList list
)
613 debug() << "inserting... " << row
<< ' ' << list
.count();
617 beginInsertRows( QModelIndex(), row
, row
+ list
.size() - 1 );
619 foreach( TrackPtr track
, list
)
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
);
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();
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
) );
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() );
674 Model::removeRowsCommand( int position
, int rows
)
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 );
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() );
694 Amarok::actionCollection()->action( "playlist_clear" )->setEnabled( !m_items
.isEmpty() );
697 bool activeRowChanged
= true;
698 bool oldActiveRow
= m_activeRow
;
699 if( m_activeRow
>= position
&& m_activeRow
< ( position
+ rows
) )
701 else if( m_activeRow
>= position
)
702 m_activeRow
= m_activeRow
- position
;
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 ) );
715 emit
playlistCountChanged( rowCount() );
720 Model::queryDone() //Slot
722 QueryMaker
*qm
= dynamic_cast<QueryMaker
*>( sender() );
725 m_queryMap
.remove( qm
);
731 Model::newResultReady( const QString
&collectionId
, const Meta::TrackList
&tracks
) //Slot
733 Q_UNUSED( collectionId
)
734 QueryMaker
*qm
= dynamic_cast<QueryMaker
*>( sender() );
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
)
768 void Model::moveRow(int row
, int to
)
771 m_items
.move( row
, to
);
778 void Model::regroupAlbums()
781 m_albumGroups
.clear();
782 m_lastAddedTrackAlbum
= AlbumPtr();
786 debug() << "number of rows: " << m_items
.count();
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() ) {
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
);
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
);
825 Playlist::Model
* playlistModel() { return Playlist::Model::s_instance
; }
835 #include "PlaylistModel.moc"