1 /****************************************************************************************
2  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
3  * Copyright (c) 2007-2009 Nikolaj Hald Nielsen <nhn@kde.org>                           *
4  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
5  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
6  * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
7  * Copyright (c) 2010 Dennis Francis <dennisfrancis.in@gmail.com>                       *
8  *                                                                                      *
9  * This program is free software; you can redistribute it and/or modify it under        *
10  * the terms of the GNU General Public License as published by the Free Software        *
11  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
12  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
13  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
14  * version 3 of the license.                                                            *
15  *                                                                                      *
16  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
17  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
18  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
19  *                                                                                      *
20  * You should have received a copy of the GNU General Public License along with         *
21  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
22  ****************************************************************************************/
23 
24 #define DEBUG_PREFIX "Playlist::Model"
25 
26 #include "PlaylistModel.h"
27 
28 #include "core/support/Amarok.h"
29 #include "SvgHandler.h"
30 #include "amarokconfig.h"
31 #include "AmarokMimeData.h"
32 #include "core/capabilities/ReadLabelCapability.h"
33 #include "core/support/Debug.h"
34 #include "EngineController.h"
35 #include "core/capabilities/MultiSourceCapability.h"
36 #include "core/capabilities/SourceInfoCapability.h"
37 #include "core/collections/Collection.h"
38 #include "core/meta/Statistics.h"
39 #include "core/meta/support/MetaUtility.h"
40 #include "PlaylistDefines.h"
41 #include "PlaylistActions.h"
42 #include "PlaylistController.h"
43 #include "PlaylistItem.h"
44 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
45 #include "core-impl/support/TrackLoader.h"
46 #include "playlist/UndoCommands.h"
47 
48 #include <KLocalizedString>
49 #include <KIconLoader>
50 
51 #include <QAction>
52 #include <QTimer>
53 #include <QDate>
54 #include <QStringList>
55 #include <QUrl>
56 
57 
58 #define TOOLTIP_STATIC_LINEBREAK 50
59 
60 bool Playlist::Model::s_tooltipColumns[NUM_COLUMNS];
61 bool Playlist::Model::s_showToolTip;
62 
63 // ------- helper functions for the tooltip
64 
65 static bool
fitsInOneLineHTML(const QString & text)66 fitsInOneLineHTML(const QString& text)
67 {
68     // The size of the normal, standard line
69     const int lnSize = TOOLTIP_STATIC_LINEBREAK;
70     return (text.size() <= lnSize);
71 }
72 
73 static QString
breakLongLinesHTML(const QString & origText)74 breakLongLinesHTML( const QString &origText )
75 {
76     // filter-out HTML tags..
77     QString text( origText );
78     text.replace( '&', QLatin1String("&amp;") ); // needs to be first, obviously
79     text.replace( '<', QLatin1String("&lt;") );
80     text.replace( '>', QLatin1String("&gt;") );
81 
82     // Now let's break up long lines so that the tooltip doesn't become hideously large
83     if( fitsInOneLineHTML( text ) )
84         // If the text is not too long, return it as it is
85         return text;
86     else
87     {
88         const int lnSize = TOOLTIP_STATIC_LINEBREAK;
89         QString textInLines;
90 
91         QStringList words = text.trimmed().split(' ');
92         int lineLength = 0;
93         while(words.size() > 0)
94         {
95             QString word = words.first();
96             // Let's check if the next word makes the current line too long.
97             if (lineLength + word.size() + 1 > lnSize)
98             {
99                 if (lineLength > 0)
100                 {
101                     textInLines += QLatin1String("<br/>");
102                 }
103                 lineLength = 0;
104                 // Let's check if the next word is not too long for the new line to contain
105                 // If it is, cut it
106                 while (word.size() > lnSize)
107                 {
108                     QString wordPart = word;
109                     wordPart.resize(lnSize);
110                     word.remove(0,lnSize);
111                     textInLines += wordPart + "<br/>";
112                 }
113             }
114             textInLines += word + ' ';
115             lineLength += word.size() + 1;
116             words.removeFirst();
117         }
118         return textInLines.trimmed();
119     }
120 }
121 
122 /**
123 * Prepares a row for the playlist tooltips consisting of an icon representing
124 * an mp3 tag and its value
125 * @param column The column used to display the icon
126 * @param value The QString value to be shown
127 * @param force If @c true, allows to set empty values
128 * @return The line to be shown or an empty QString if the value is null
129 */
130 static QString
HTMLLine(const Playlist::Column & column,const QString & value,bool force=false)131 HTMLLine( const Playlist::Column& column, const QString& value, bool force = false )
132 {
133     if( !value.isEmpty() || force )
134     {
135         QString line;
136         line += QLatin1String("<tr><td align=\"right\">");
137         line += "<img src=\""+KIconLoader::global()->iconPath( Playlist::iconName( column ), -16)+"\" />";
138         line += QLatin1String("</td><td align=\"left\">");
139         line += breakLongLinesHTML( value );
140         line += QLatin1String("</td></tr>");
141         return line;
142     }
143     else
144         return QString();
145 }
146 
147 /**
148 * Prepares a row for the playlist tooltips consisting of an icon representing
149 * an mp3 tag and its value
150 * @param column The column used to display the icon
151 * @param value The integer value to be shown
152 * @param force If @c true, allows to set non-positive values
153 * @return The line to be shown or an empty QString if the value is 0
154 */
155 static QString
HTMLLine(const Playlist::Column & column,const int value,bool force=false)156 HTMLLine( const Playlist::Column& column, const int value, bool force = false )
157 {
158     // there is currently no numeric meta-data that would have sense if it were negative.
159     // also, zero denotes not available, unknown etc; don't show these unless forced.
160     if( value > 0 || force )
161     {
162         return HTMLLine( column, QString::number( value ) );
163     }
164     else
165         return QString();
166 }
167 
168 
Model(QObject * parent)169 Playlist::Model::Model( QObject *parent )
170         : QAbstractListModel( parent )
171         , m_activeRow( -1 )
172         , m_totalLength( 0 )
173         , m_totalSize( 0 )
174         , m_setStateOfItem_batchMinRow( -1 )
175         , m_saveStateTimer( new QTimer(this) )
176 {
177     DEBUG_BLOCK
178 
179     m_saveStateTimer->setInterval( 5000 );
180     m_saveStateTimer->setSingleShot( true );
181     connect( m_saveStateTimer, &QTimer::timeout,
182              this, &Playlist::Model::saveState );
183     connect( this, &Playlist::Model::modelReset,
184              this, &Playlist::Model::queueSaveState );
185     connect( this, &Playlist::Model::dataChanged,
186              this, &Playlist::Model::queueSaveState );
187     connect( this, &Playlist::Model::rowsInserted,
188              this, &Playlist::Model::queueSaveState );
189     connect( this, &Playlist::Model::rowsMoved,
190              this, &Playlist::Model::queueSaveState );
191     connect( this, &Playlist::Model::rowsRemoved,
192              this, &Playlist::Model::queueSaveState );
193 }
194 
~Model()195 Playlist::Model::~Model()
196 {
197     DEBUG_BLOCK
198 
199     // Save current playlist
200     exportPlaylist( Amarok::defaultPlaylistPath() );
201 
202     qDeleteAll( m_items );
203 }
204 
205 void
saveState()206 Playlist::Model::saveState()
207 {
208     exportPlaylist( Amarok::defaultPlaylistPath() );
209 }
210 
211 void
queueSaveState()212 Playlist::Model::queueSaveState()
213 {
214     if ( !m_saveStateTimer->isActive() )
215         m_saveStateTimer->start();
216 }
217 
218 void
insertTracksFromTrackLoader(const Meta::TrackList & tracks)219 Playlist::Model::insertTracksFromTrackLoader( const Meta::TrackList &tracks )
220 {
221     QObject *loader = sender();
222     if( !sender() )
223     {
224         warning() << __PRETTY_FUNCTION__ << "can only be connected to TrackLoader";
225         return;
226     }
227     int insertRow = loader->property( "beginRow" ).toInt();
228     Controller::instance()->insertTracks( insertRow, tracks );
229 }
230 
231 QVariant
headerData(int section,Qt::Orientation orientation,int role) const232 Playlist::Model::headerData( int section, Qt::Orientation orientation, int role ) const
233 {
234     Q_UNUSED( orientation );
235 
236     if ( role != Qt::DisplayRole )
237         return QVariant();
238 
239     return columnName( static_cast<Playlist::Column>( section ) );
240 }
241 
242 void
setTooltipColumns(bool columns[])243 Playlist::Model::setTooltipColumns( bool columns[] )
244 {
245     for( int i=0; i<Playlist::NUM_COLUMNS; ++i )
246         s_tooltipColumns[i] = columns[i];
247 }
248 
249 void
enableToolTip(bool enable)250 Playlist::Model::enableToolTip( bool enable )
251 {
252     s_showToolTip = enable;
253 }
254 
255 QString
tooltipFor(Meta::TrackPtr track) const256 Playlist::Model::tooltipFor( Meta::TrackPtr track ) const
257 {
258     QString text;
259     // get the shared pointers now to be thread safe
260     Meta::ArtistPtr artist = track->artist();
261     Meta::AlbumPtr album = track->album();
262     Meta::ArtistPtr albumArtist = album ? album->albumArtist() : Meta::ArtistPtr();
263     Meta::GenrePtr genre = track->genre();
264     Meta::ComposerPtr composer = track->composer();
265     Meta::YearPtr year = track->year();
266     Meta::StatisticsPtr statistics = track->statistics();
267 
268     if( !track->isPlayable() )
269         text += i18n( "<b>Note:</b> This track is not playable.<br>%1", track->notPlayableReason() );
270 
271     if( s_tooltipColumns[Playlist::Title] )
272         text += HTMLLine( Playlist::Title, track->name() );
273 
274     if( s_tooltipColumns[Playlist::Artist] && artist )
275         text += HTMLLine( Playlist::Artist, artist->name() );
276 
277     // only show albumArtist when different from artist (it should suffice to compare pointers)
278     if( s_tooltipColumns[Playlist::AlbumArtist] && albumArtist && albumArtist != artist )
279         text += HTMLLine( Playlist::AlbumArtist, albumArtist->name() );
280 
281     if( s_tooltipColumns[Playlist::Album] && album )
282         text += HTMLLine( Playlist::Album, album->name() );
283 
284     if( s_tooltipColumns[Playlist::DiscNumber] )
285         text += HTMLLine( Playlist::DiscNumber, track->discNumber() );
286 
287     if( s_tooltipColumns[Playlist::TrackNumber] )
288         text += HTMLLine( Playlist::TrackNumber, track->trackNumber() );
289 
290     if( s_tooltipColumns[Playlist::Composer] && composer )
291         text += HTMLLine( Playlist::Composer, composer->name() );
292 
293     if( s_tooltipColumns[Playlist::Genre] && genre )
294         text += HTMLLine( Playlist::Genre, genre->name() );
295 
296     if( s_tooltipColumns[Playlist::Year] && year && year->year() > 0 )
297         text += HTMLLine( Playlist::Year, year->year() );
298 
299     if( s_tooltipColumns[Playlist::Bpm] )
300         text += HTMLLine( Playlist::Bpm, track->bpm() );
301 
302     if( s_tooltipColumns[Playlist::Comment]) {
303         if ( !(fitsInOneLineHTML( track->comment() ) ) )
304             text += HTMLLine( Playlist::Comment, i18n( "(...)" ) );
305         else
306             text += HTMLLine( Playlist::Comment, track->comment() );
307     }
308 
309     if( s_tooltipColumns[Playlist::Labels] && !track->labels().empty() )
310     {
311         QStringList labels;
312         foreach( Meta::LabelPtr label, track->labels() )
313         {
314             if( label )
315                 labels << label->name();
316         }
317         text += HTMLLine( Playlist::Labels, labels.join( QStringLiteral(", ") ) );
318     }
319 
320     if( s_tooltipColumns[Playlist::Score] )
321         text += HTMLLine( Playlist::Score, statistics->score() );
322 
323     if( s_tooltipColumns[Playlist::Rating] )
324         text += HTMLLine( Playlist::Rating, QString::number( statistics->rating()/2.0 ) );
325 
326     if( s_tooltipColumns[Playlist::PlayCount] )
327         text += HTMLLine( Playlist::PlayCount, statistics->playCount(), true );
328 
329     if( s_tooltipColumns[Playlist::LastPlayed] && statistics->lastPlayed().isValid() )
330         text += HTMLLine( Playlist::LastPlayed, QLocale().toString( statistics->lastPlayed() ) );
331 
332     if( s_tooltipColumns[Playlist::Bitrate] && track->bitrate() )
333         text += HTMLLine( Playlist::Bitrate, i18nc( "%1: bitrate", "%1 kbps", track->bitrate() ) );
334 
335     if( text.isEmpty() )
336         text = i18n( "No extra information available" );
337     else
338         text = QString("<table>"+ text +"</table>");
339 
340     return text;
341 }
342 
343 QVariant
data(const QModelIndex & index,int role) const344 Playlist::Model::data( const QModelIndex& index, int role ) const
345 {
346     int row = index.row();
347 
348     if ( !index.isValid() || !rowExists( row ) )
349         return QVariant();
350 
351     if ( role == UniqueIdRole )
352         return QVariant( idAt( row ) );
353 
354     else if ( role == ActiveTrackRole )
355         return ( row == m_activeRow );
356 
357     else if ( role == TrackRole && m_items.at( row )->track() )
358         return QVariant::fromValue( m_items.at( row )->track() );
359 
360     else if ( role == StateRole )
361         return m_items.at( row )->state();
362 
363     else if ( role == QueuePositionRole )
364         return Actions::instance()->queuePosition( idAt( row ) ) + 1;
365 
366     else if ( role == InCollectionRole )
367         return  m_items.at( row )->track()->inCollection();
368 
369     else if ( role == MultiSourceRole )
370         return  m_items.at( row )->track()->has<Capabilities::MultiSourceCapability>();
371 
372     else if ( role == StopAfterTrackRole )
373         return Actions::instance()->willStopAfterTrack( idAt( row ) );
374 
375     else if ( role == Qt::ToolTipRole )
376     {
377         Meta::TrackPtr track = m_items.at( row )->track();
378         if( s_showToolTip )
379             return tooltipFor( track );
380         else if( !track->isPlayable() )
381             return i18n( "<b>Note:</b> This track is not playable.<br>%1", track->notPlayableReason() );
382     }
383 
384     else if ( role == Qt::DisplayRole )
385     {
386         Meta::TrackPtr track = m_items.at( row )->track();
387         Meta::AlbumPtr album = track->album();
388         Meta::StatisticsPtr statistics = track->statistics();
389         switch ( index.column() )
390         {
391             case PlaceHolder:
392                 break;
393             case Album:
394             {
395                 if( album )
396                     return album->name();
397                 break;
398             }
399             case AlbumArtist:
400             {
401                 if( album )
402                 {
403                     Meta::ArtistPtr artist = album->albumArtist();
404                     if( artist )
405                         return artist->name();
406                 }
407                 break;
408             }
409             case Artist:
410             {
411                 Meta::ArtistPtr artist = track->artist();
412                 if( artist )
413                     return artist->name();
414                 break;
415             }
416             case Bitrate:
417             {
418                 return Meta::prettyBitrate( track->bitrate() );
419             }
420             case Bpm:
421             {
422                 if( track->bpm() > 0.0 )
423                     return QString::number( track->bpm() );
424                 break;
425             }
426             case Comment:
427             {
428                 return track->comment();
429             }
430             case Composer:
431             {
432                 Meta::ComposerPtr composer = track->composer();
433                 if( composer )
434                     return composer->name();
435                 break;
436             }
437             case CoverImage:
438             {
439                 if( album )
440                     return The::svgHandler()->imageWithBorder( album, 100 ); //FIXME:size?
441                 break;
442             }
443             case Directory:
444             {
445                 if( track->playableUrl().isLocalFile() )
446                     return track->playableUrl().adjusted(QUrl::RemoveFilename).path();
447                 break;
448             }
449             case DiscNumber:
450             {
451                 if( track->discNumber() > 0 )
452                     return track->discNumber();
453                 break;
454             }
455             case Filename:
456             {
457 
458                 if( track->playableUrl().isLocalFile() )
459                     return track->playableUrl().fileName();
460                 break;
461             }
462             case Filesize:
463             {
464                 return Meta::prettyFilesize( track->filesize() );
465             }
466             case Genre:
467             {
468                 Meta::GenrePtr genre = track->genre();
469                 if( genre )
470                     return genre->name();
471                 break;
472             }
473             case GroupLength:
474             {
475                 return Meta::secToPrettyTime( 0 );
476             }
477             case GroupTracks:
478             {
479                 return QString();
480             }
481             case Labels:
482             {
483                 if( track )
484                 {
485                     QStringList labelNames;
486                     foreach( const Meta::LabelPtr &label, track->labels() )
487                     {
488                         labelNames << label->prettyName();
489                     }
490                     return labelNames.join( QStringLiteral(", ") );
491                 }
492                 break;
493             }
494             case LastPlayed:
495             {
496                 if( statistics->playCount() == 0 )
497                     return i18nc( "The amount of time since last played", "Never" );
498                 else if( statistics->lastPlayed().isValid() )
499                     return Amarok::verboseTimeSince( statistics->lastPlayed() );
500                 else
501                     return i18nc( "When this track was last played", "Unknown" );
502             }
503             case Length:
504             {
505                 return Meta::msToPrettyTime( track->length() );
506             }
507             case LengthInSeconds:
508             {
509                 return track->length() / 1000;
510             }
511             case Mood:
512             {
513                 return QString(); //FIXME
514             }
515             case PlayCount:
516             {
517                 return statistics->playCount();
518             }
519             case Rating:
520             {
521                 return statistics->rating();
522             }
523             case SampleRate:
524             {
525                 if( track->sampleRate() > 0 )
526                     return track->sampleRate();
527                 break;
528             }
529             case Score:
530             {
531                 return int( statistics->score() ); // Cast to int, as we don't need to show the decimals in the view..
532             }
533             case Source:
534             {
535                 QString sourceName;
536                 Capabilities::SourceInfoCapability *sic = track->create<Capabilities::SourceInfoCapability>();
537                 if ( sic )
538                 {
539                     sourceName = sic->sourceName();
540                     delete sic;
541                 }
542                 else
543                 {
544                     sourceName = track->collection() ? track->collection()->prettyName() : QString();
545                 }
546                 return sourceName;
547             }
548             case SourceEmblem:
549             {
550                 QPixmap emblem;
551                 Capabilities::SourceInfoCapability *sic = track->create<Capabilities::SourceInfoCapability>();
552                 if ( sic )
553                 {
554                     QString source = sic->sourceName();
555                     if ( !source.isEmpty() )
556                         emblem = sic->emblem();
557                     delete sic;
558                 }
559                 return emblem;
560             }
561             case Title:
562             {
563                 return track->prettyName();
564             }
565             case TitleWithTrackNum:
566             {
567                 QString trackString;
568                 QString trackName = track->prettyName();
569                 if( track->trackNumber() > 0 )
570                 {
571                     QString trackNumber = QString::number( track->trackNumber() );
572                     trackString =  QString( trackNumber + " - " + trackName );
573                 } else
574                     trackString = trackName;
575 
576                 return trackString;
577             }
578             case TrackNumber:
579             {
580                 if( track->trackNumber() > 0 )
581                     return track->trackNumber();
582                 break;
583             }
584             case Type:
585             {
586                 return track->type();
587             }
588             case Year:
589             {
590                 Meta::YearPtr year = track->year();
591                 if( year && year->year() > 0 )
592                     return year->year();
593                 break;
594             }
595             default:
596                 return QVariant(); // returning a variant instead of a string inside a variant is cheaper
597 
598         }
599     }
600     // else
601     return QVariant();
602 }
603 
604 Qt::DropActions
supportedDropActions() const605 Playlist::Model::supportedDropActions() const
606 {
607     return Qt::MoveAction | QAbstractListModel::supportedDropActions();
608 }
609 
610 Qt::ItemFlags
flags(const QModelIndex & index) const611 Playlist::Model::flags( const QModelIndex &index ) const
612 {
613     if ( index.isValid() )
614         return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable );
615     return Qt::ItemIsDropEnabled;
616 }
617 
618 QStringList
mimeTypes() const619 Playlist::Model::mimeTypes() const
620 {
621     QStringList ret = QAbstractListModel::mimeTypes();
622     ret << AmarokMimeData::TRACK_MIME;
623     ret << QStringLiteral("text/uri-list"); //we do accept urls
624     return ret;
625 }
626 
627 QMimeData*
mimeData(const QModelIndexList & indexes) const628 Playlist::Model::mimeData( const QModelIndexList &indexes ) const
629 {
630     AmarokMimeData* mime = new AmarokMimeData();
631     Meta::TrackList selectedTracks;
632 
633     foreach( const QModelIndex &it, indexes )
634     selectedTracks << m_items.at( it.row() )->track();
635 
636     mime->setTracks( selectedTracks );
637     return mime;
638 }
639 
640 bool
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int,const QModelIndex & parent)641 Playlist::Model::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex &parent )
642 {
643     if ( action == Qt::IgnoreAction )
644         return true;
645 
646     int beginRow;
647     if ( row != -1 )
648         beginRow = row;
649     else if ( parent.isValid() )
650         beginRow = parent.row();
651     else
652         beginRow = m_items.size();
653 
654     if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
655     {
656         debug() << "this is a track";
657         const AmarokMimeData* trackListDrag = qobject_cast<const AmarokMimeData*>( data );
658         if( trackListDrag )
659         {
660 
661             Meta::TrackList tracks = trackListDrag->tracks();
662             qStableSort( tracks.begin(), tracks.end(), Meta::Track::lessThan );
663 
664             The::playlistController()->insertTracks( beginRow, tracks );
665         }
666         return true;
667     }
668     else if( data->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
669     {
670         debug() << "this is a playlist";
671         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
672         if( dragList )
673             The::playlistController()->insertPlaylists( beginRow, dragList->playlists() );
674         return true;
675     }
676     else if( data->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) )
677     {
678         debug() << "this is a podcast episode";
679         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
680         if( dragList )
681         {
682             Meta::TrackList tracks;
683             foreach( Podcasts::PodcastEpisodePtr episode, dragList->podcastEpisodes() )
684                 tracks << Meta::TrackPtr::staticCast( episode );
685             The::playlistController()->insertTracks( beginRow, tracks );
686         }
687         return true;
688     }
689     else if( data->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
690     {
691         debug() << "this is a podcast channel";
692         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
693         if( dragList )
694         {
695             Meta::TrackList tracks;
696             foreach( Podcasts::PodcastChannelPtr channel, dragList->podcastChannels() )
697                 foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
698                     tracks << Meta::TrackPtr::staticCast( episode );
699             The::playlistController()->insertTracks( beginRow, tracks );
700         }
701         return true;
702     }
703     else if( data->hasUrls() )
704     {
705         debug() << "this is _something_ with a url....";
706         TrackLoader *dl = new TrackLoader(); // auto-deletes itself
707         dl->setProperty( "beginRow", beginRow );
708         connect( dl, &TrackLoader::finished, this, &Model::insertTracksFromTrackLoader );
709         dl->init( data->urls() );
710         return true;
711     }
712 
713     debug() << "I have no idea what the hell this is...";
714     return false;
715 }
716 
717 void
setActiveRow(int row)718 Playlist::Model::setActiveRow( int row )
719 {
720     if ( rowExists( row ) )
721     {
722         setStateOfRow( row, Item::Played );
723         m_activeRow = row;
724         Q_EMIT activeTrackChanged( m_items.at( row )->id() );
725     }
726     else
727     {
728         m_activeRow = -1;
729         Q_EMIT activeTrackChanged( 0 );
730     }
731 }
732 
733 void
emitQueueChanged()734 Playlist::Model::emitQueueChanged()
735 {
736     Q_EMIT queueChanged();
737 }
738 
739 int
queuePositionOfRow(int row)740 Playlist::Model::queuePositionOfRow( int row )
741 {
742     return Actions::instance()->queuePosition( idAt( row ) ) + 1;
743 }
744 
745 Playlist::Item::State
stateOfRow(int row) const746 Playlist::Model::stateOfRow( int row ) const
747 {
748     if ( rowExists( row ) )
749         return m_items.at( row )->state();
750     else
751         return Item::Invalid;
752 }
753 
754 bool
containsTrack(const Meta::TrackPtr & track) const755 Playlist::Model::containsTrack( const Meta::TrackPtr& track ) const
756 {
757     foreach( Item* i, m_items )
758     {
759         if ( i->track() == track )
760             return true;
761     }
762     return false;
763 }
764 
765 int
firstRowForTrack(const Meta::TrackPtr & track) const766 Playlist::Model::firstRowForTrack( const Meta::TrackPtr& track ) const
767 {
768     int row = 0;
769     foreach( Item* i, m_items )
770     {
771         if ( i->track() == track )
772             return row;
773         row++;
774     }
775     return -1;
776 }
777 
778 QSet<int>
allRowsForTrack(const Meta::TrackPtr & track) const779 Playlist::Model::allRowsForTrack( const Meta::TrackPtr& track ) const
780 {
781     QSet<int> trackRows;
782 
783     int row = 0;
784     foreach( Item* i, m_items )
785     {
786         if ( i->track() == track )
787             trackRows.insert( row );
788         row++;
789     }
790     return trackRows;
791 }
792 
793 Meta::TrackPtr
trackAt(int row) const794 Playlist::Model::trackAt( int row ) const
795 {
796     if ( rowExists( row ) )
797         return m_items.at( row )->track();
798     else
799         return Meta::TrackPtr();
800 }
801 
802 Meta::TrackPtr
activeTrack() const803 Playlist::Model::activeTrack() const
804 {
805     if ( rowExists( m_activeRow ) )
806         return m_items.at( m_activeRow )->track();
807     else
808         return Meta::TrackPtr();
809 }
810 
811 int
rowForId(const quint64 id) const812 Playlist::Model::rowForId( const quint64 id ) const
813 {
814     return m_items.indexOf( m_itemIds.value( id ) );    // Returns -1 on miss, same as our API.
815 }
816 
817 Meta::TrackPtr
trackForId(const quint64 id) const818 Playlist::Model::trackForId( const quint64 id ) const
819 {
820     Item* item = m_itemIds.value( id, 0 );
821     if ( item )
822         return item->track();
823     else
824         return Meta::TrackPtr();
825 }
826 
827 quint64
idAt(const int row) const828 Playlist::Model::idAt( const int row ) const
829 {
830     if ( rowExists( row ) )
831         return m_items.at( row )->id();
832     else
833         return 0;
834 }
835 
836 quint64
activeId() const837 Playlist::Model::activeId() const
838 {
839     if ( rowExists( m_activeRow ) )
840         return m_items.at( m_activeRow )->id();
841     else
842         return 0;
843 }
844 
845 Playlist::Item::State
stateOfId(quint64 id) const846 Playlist::Model::stateOfId( quint64 id ) const
847 {
848     Item* item = m_itemIds.value( id, 0 );
849     if ( item )
850         return item->state();
851     else
852         return Item::Invalid;
853 }
854 
855 void
metadataChanged(const Meta::TrackPtr & track)856 Playlist::Model::metadataChanged(const Meta::TrackPtr &track )
857 {
858     int row = 0;
859     foreach( Item* i, m_items )
860     {
861         if ( i->track() == track )
862         {
863             // ensure that we really have the correct album subscribed (in case it changed)
864             Meta::AlbumPtr album = track->album();
865             if( album )
866                 subscribeTo( album );
867 
868             Q_EMIT dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
869         }
870         row++;
871     }
872 }
873 
874 void
metadataChanged(const Meta::AlbumPtr & album)875 Playlist::Model::metadataChanged(const Meta::AlbumPtr &album )
876 {
877     // Mainly to get update about changed covers
878 
879     // -- search for all the tracks having this album
880     bool found = false;
881     const int size = m_items.size();
882     for ( int i = 0; i < size; i++ )
883     {
884         if ( m_items.at( i )->track()->album() == album )
885         {
886             Q_EMIT dataChanged( index( i, 0 ), index( i, columnCount() - 1 ) );
887             found = true;
888             debug()<<"Metadata updated for album"<<album->prettyName();
889         }
890     }
891 
892     // -- unsubscribe if we don't have a track from that album left.
893     // this can happen if the album of a track changed
894     if( !found )
895         unsubscribeFrom( album );
896 }
897 
898 bool
exportPlaylist(const QString & path,bool relative)899 Playlist::Model::exportPlaylist( const QString &path, bool relative )
900 {
901     // check queue state
902     QQueue<quint64> queueIds = The::playlistActions()->queue();
903     QList<int> queued;
904     foreach( quint64 id, queueIds ) {
905       queued << rowForId( id );
906     }
907     return Playlists::exportPlaylistFile( tracks(), QUrl::fromLocalFile(path), relative, queued);
908 }
909 
910 Meta::TrackList
tracks()911 Playlist::Model::tracks()
912 {
913     Meta::TrackList tl;
914     foreach( Item* item, m_items )
915         tl << item->track();
916     return tl;
917 }
918 
919 QString
prettyColumnName(Column index)920 Playlist::Model::prettyColumnName( Column index ) //static
921 {
922     switch ( index )
923     {
924     case Filename:   return i18nc( "The name of the file this track is stored in", "Filename" );
925     case Title:      return i18n( "Title" );
926     case Artist:     return i18n( "Artist" );
927     case AlbumArtist: return i18n( "Album Artist" );
928     case Composer:   return i18n( "Composer" );
929     case Year:       return i18n( "Year" );
930     case Album:      return i18n( "Album" );
931     case DiscNumber: return i18n( "Disc Number" );
932     case TrackNumber: return i18nc( "The Track number for this item", "Track" );
933     case Bpm:        return i18n( "BPM" );
934     case Genre:      return i18n( "Genre" );
935     case Comment:    return i18n( "Comment" );
936     case Directory:  return i18nc( "The location on disc of this track", "Directory" );
937     case Type:       return i18n( "Type" );
938     case Length:     return i18n( "Length" );
939     case Bitrate:    return i18n( "Bitrate" );
940     case SampleRate: return i18n( "Sample Rate" );
941     case Score:      return i18n( "Score" );
942     case Rating:     return i18n( "Rating" );
943     case PlayCount:  return i18n( "Play Count" );
944     case LastPlayed: return i18nc( "Column name", "Last Played" );
945     case Mood:       return i18n( "Mood" );
946     case Filesize:   return i18n( "File Size" );
947     default:         return QStringLiteral("This is a bug.");
948     }
949 
950 }
951 
952 
953 ////////////
954 //Private Methods
955 ////////////
956 
957 void
insertTracksCommand(const InsertCmdList & cmds)958 Playlist::Model::insertTracksCommand( const InsertCmdList& cmds )
959 {
960     if ( cmds.size() < 1 )
961         return;
962 
963     setAllNewlyAddedToUnplayed();
964 
965     int activeShift = 0;
966     int min = m_items.size() + cmds.size();
967     int max = 0;
968     int begin = cmds.at( 0 ).second;
969     foreach( const InsertCmd &ic, cmds )
970     {
971         min = qMin( min, ic.second );
972         max = qMax( max, ic.second );
973         activeShift += ( begin <= m_activeRow ) ? 1 : 0;
974     }
975 
976     // actually do the insertion
977     beginInsertRows( QModelIndex(), min, max );
978     foreach( const InsertCmd &ic, cmds )
979     {
980         Meta::TrackPtr track = ic.first;
981         m_totalLength += track->length();
982         m_totalSize += track->filesize();
983         subscribeTo( track );
984         Meta::AlbumPtr album = track->album();
985         if( album )
986             subscribeTo( album );
987 
988         Item* newitem = new Item( track );
989         m_items.insert( ic.second, newitem );
990         m_itemIds.insert( newitem->id(), newitem );
991     }
992     endInsertRows();
993 
994     if( m_activeRow >= 0 )
995         m_activeRow += activeShift;
996     else
997     {
998         EngineController *engine = The::engineController();
999         if( engine ) // test cases might create a playlist without having an EngineController
1000         {
1001             const Meta::TrackPtr engineTrack = engine->currentTrack();
1002             if( engineTrack )
1003             {
1004                 int engineRow = firstRowForTrack( engineTrack );
1005                 if( engineRow > -1 )
1006                     setActiveRow( engineRow );
1007             }
1008         }
1009     }
1010 }
1011 
1012 static bool
removeCmdLessThanByRow(const Playlist::RemoveCmd & left,const Playlist::RemoveCmd & right)1013 removeCmdLessThanByRow( const Playlist::RemoveCmd &left, const Playlist::RemoveCmd &right )
1014 {
1015     return left.second < right.second;
1016 }
1017 
1018 void
removeTracksCommand(const RemoveCmdList & passedCmds)1019 Playlist::Model::removeTracksCommand( const RemoveCmdList &passedCmds )
1020 {
1021     DEBUG_BLOCK
1022     if ( passedCmds.size() < 1 )
1023         return;
1024 
1025     if ( passedCmds.size() == m_items.size() )
1026     {
1027         clearCommand();
1028         return;
1029     }
1030 
1031     // sort tracks to remove by their row
1032     RemoveCmdList cmds( passedCmds );
1033     qSort( cmds.begin(), cmds.end(), removeCmdLessThanByRow );
1034 
1035     // update the active row
1036     if( m_activeRow >= 0 )
1037     {
1038         int activeShift = 0;
1039         foreach( const RemoveCmd &rc, cmds )
1040         {
1041             if( rc.second < m_activeRow )
1042                 activeShift++;
1043             else if( rc.second == m_activeRow )
1044                 m_activeRow = -1; // disappeared
1045             else
1046                 break; // we got over it, nothing left to do
1047         }
1048         if( m_activeRow >= 0 ) // not deleted
1049             m_activeRow -= activeShift;
1050     }
1051 
1052     QSet<Meta::TrackPtr> trackUnsubscribeCandidates;
1053     QSet<Meta::AlbumPtr> albumUnsubscribeCandidates;
1054 
1055     QListIterator<RemoveCmd> it( cmds );
1056     int removedRows = 0;
1057     while( it.hasNext() )
1058     {
1059         int startRow = it.next().second;
1060         int endRow = startRow;
1061 
1062         // find consecutive runs of rows, this is important to group begin/endRemoveRows(),
1063         // which are very costly when there are many proxymodels and a view above.
1064         while( it.hasNext() && it.peekNext().second == endRow + 1 )
1065         {
1066             it.next();
1067             endRow++;
1068         }
1069 
1070         beginRemoveRows( QModelIndex(), startRow - removedRows, endRow - removedRows );
1071         for( int row = startRow; row <= endRow; row++ )
1072         {
1073             Item *removedItem = m_items.at( row - removedRows );
1074             m_items.removeAt( row - removedRows );
1075             m_itemIds.remove( removedItem->id() );
1076 
1077             const Meta::TrackPtr &track = removedItem->track();
1078             // update totals here so they're right when endRemoveRows() called
1079             m_totalLength -= track->length();
1080             m_totalSize -= track->filesize();
1081             trackUnsubscribeCandidates.insert( track );
1082             Meta::AlbumPtr album = track->album();
1083             if( album )
1084                 albumUnsubscribeCandidates.insert( album );
1085 
1086             delete removedItem; // note track is by reference, needs removedItem alive
1087             removedRows++;
1088         }
1089         endRemoveRows();
1090     }
1091 
1092     // unsubscribe from tracks no longer present in playlist
1093     foreach( Meta::TrackPtr track, trackUnsubscribeCandidates )
1094     {
1095         if( !containsTrack( track ) )
1096             unsubscribeFrom( track );
1097     }
1098 
1099     // unsubscribe from albums no longer present im playlist
1100     QSet<Meta::AlbumPtr> remainingAlbums;
1101     foreach( const Item *item, m_items )
1102     {
1103         Meta::AlbumPtr album = item->track()->album();
1104         if( album )
1105             remainingAlbums.insert( album );
1106     }
1107     foreach( Meta::AlbumPtr album, albumUnsubscribeCandidates )
1108     {
1109         if( !remainingAlbums.contains( album ) )
1110             unsubscribeFrom( album );
1111     }
1112 
1113     // make sure that there are enough tracks if we just removed from a dynamic playlist.
1114     // This call needs to be delayed or else we would mess up the undo queue
1115     // BUG: 259675
1116     // FIXME: removing the track and normalizing the playlist should be grouped together
1117     //        so that an undo operation undos both.
1118     QTimer::singleShot(0, Playlist::Actions::instance(), &Playlist::Actions::normalizeDynamicPlaylist);
1119 }
1120 
1121 
clearCommand()1122 void Playlist::Model::clearCommand()
1123 {
1124     setActiveRow( -1 );
1125 
1126     beginRemoveRows( QModelIndex(), 0, rowCount() - 1 );
1127 
1128     m_totalLength = 0;
1129     m_totalSize = 0;
1130 
1131     qDeleteAll( m_items );
1132     m_items.clear();
1133     m_itemIds.clear();
1134 
1135     endRemoveRows();
1136 }
1137 
1138 
1139 // Note: this function depends on 'MoveCmdList' to be a complete "cycle", in the sense
1140 // that if row A is moved to row B, another row MUST be moved to row A.
1141 // Very strange API design IMHO, because it forces our caller to e.g. move ALL ROWS in
1142 // the playlist to move row 0 to the last row. This function should just have been
1143 // equivalent to a 'removeTracks()' followed by an 'insertTracks()' IMHO.  --Nanno
1144 
1145 void
moveTracksCommand(const MoveCmdList & cmds,bool reverse)1146 Playlist::Model::moveTracksCommand( const MoveCmdList& cmds, bool reverse )
1147 {
1148     DEBUG_BLOCK
1149     debug()<<"moveTracksCommand:"<<cmds.size()<<reverse;
1150 
1151     if ( cmds.size() < 1 )
1152         return;
1153 
1154     int min = INT_MAX;
1155     int max = INT_MIN;
1156     foreach( const MoveCmd &rc, cmds )
1157     {
1158         min = qMin( min, rc.first );
1159         max = qMax( max, rc.first );
1160     }
1161 
1162     if( min < 0 || max >= m_items.size() )
1163     {
1164         error() << "Wrong row numbers given";
1165         return;
1166     }
1167 
1168     int newActiveRow = m_activeRow;
1169     QList<Item*> oldItems( m_items );
1170     if ( reverse )
1171     {
1172         foreach( const MoveCmd &mc, cmds )
1173         {
1174             m_items[mc.first] = oldItems.at( mc.second );
1175             if ( m_activeRow == mc.second )
1176                 newActiveRow = mc.first;
1177         }
1178     }
1179     else
1180     {
1181         foreach( const MoveCmd &mc, cmds )
1182         {
1183             m_items[mc.second] = oldItems.at( mc.first );
1184             if ( m_activeRow == mc.first )
1185                 newActiveRow = mc.second;
1186         }
1187     }
1188 
1189     // We have 3 choices:
1190     //   - Call 'beginMoveRows()' / 'endMoveRows()'. Drawback: we'd need to do N of them, all causing resorts etc.
1191     //   - Emit 'layoutAboutToChange' / 'layoutChanged'. Drawback: unspecific, 'changePersistentIndex()' complications.
1192     //   - Emit 'dataChanged'. Drawback: a bit inappropriate. But not wrong.
1193     Q_EMIT dataChanged( index( min, 0 ), index( max, columnCount() - 1 ) );
1194 
1195     //update the active row
1196     m_activeRow = newActiveRow;
1197 }
1198 
1199 
1200 // When doing a 'setStateOfItem_batch', we Q_EMIT 1 crude 'dataChanged' signal. If we're
1201 // unlucky, that signal may span many innocent rows that haven't changed at all.
1202 // Although that "worst case" will cause unnecessary work in our listeners "upstream", it
1203 // still has much better performance than the worst case of emitting very many tiny
1204 // 'dataChanged' signals.
1205 //
1206 // Being more clever (coalesce multiple contiguous ranges, etc.) is not worth the effort.
1207 void
setStateOfItem_batchStart()1208 Playlist::Model::setStateOfItem_batchStart()
1209 {
1210     m_setStateOfItem_batchMinRow = rowCount() + 1;
1211     m_setStateOfItem_batchMaxRow = 0;
1212 }
1213 
1214 void
setStateOfItem_batchEnd()1215 Playlist::Model::setStateOfItem_batchEnd()
1216 {
1217     if ( ( m_setStateOfItem_batchMaxRow - m_setStateOfItem_batchMinRow ) >= 0 )
1218         Q_EMIT dataChanged( index( m_setStateOfItem_batchMinRow, 0 ), index( m_setStateOfItem_batchMaxRow, columnCount() - 1 ) );
1219 
1220     m_setStateOfItem_batchMinRow = -1;
1221 }
1222 
1223 void
setStateOfItem(Item * item,int row,Item::State state)1224 Playlist::Model::setStateOfItem( Item *item, int row, Item::State state )
1225 {
1226     item->setState( state );
1227 
1228     if ( m_setStateOfItem_batchMinRow == -1 )    // If not in batch mode
1229         Q_EMIT dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
1230     else
1231     {
1232         m_setStateOfItem_batchMinRow = qMin( m_setStateOfItem_batchMinRow, row );
1233         m_setStateOfItem_batchMaxRow = qMax( m_setStateOfItem_batchMaxRow, row );
1234     }
1235 }
1236 
1237 
1238 // Unimportant TODO: the performance of this function is O(n) in playlist size.
1239 // Not a big problem, because it's called infrequently.
1240 // Can be fixed by maintaining a new member variable 'QMultiHash<Item::State, Item*>'.
1241 void
setAllNewlyAddedToUnplayed()1242 Playlist::Model::setAllNewlyAddedToUnplayed()
1243 {
1244     DEBUG_BLOCK
1245 
1246     setStateOfItem_batchStart();
1247 
1248     for ( int row = 0; row < rowCount(); row++ )
1249     {
1250         Item* item = m_items.at( row );
1251         if ( item->state() == Item::NewlyAdded )
1252             setStateOfItem( item, row, Item::Unplayed );
1253     }
1254 
1255     setStateOfItem_batchEnd();
1256 }
1257 
setAllUnplayed()1258 void Playlist::Model::setAllUnplayed()
1259 {
1260     DEBUG_BLOCK
1261 
1262     setStateOfItem_batchStart();
1263 
1264     for ( int row = 0; row < rowCount(); row++ )
1265     {
1266         Item* item = m_items.at( row );
1267         setStateOfItem( item, row, Item::Unplayed );
1268     }
1269 
1270     setStateOfItem_batchEnd();
1271 }
1272