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("&") ); // needs to be first, obviously
79 text.replace( '<', QLatin1String("<") );
80 text.replace( '>', QLatin1String(">") );
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