File indexing completed on 2024-05-05 04:48:44

0001 /****************************************************************************************
0002  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
0003  * Copyright (c) 2007-2009 Nikolaj Hald Nielsen <nhn@kde.org>                           *
0004  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
0005  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
0006  * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
0007  * Copyright (c) 2010 Dennis Francis <dennisfrancis.in@gmail.com>                       *
0008  *                                                                                      *
0009  * This program is free software; you can redistribute it and/or modify it under        *
0010  * the terms of the GNU General Public License as published by the Free Software        *
0011  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
0012  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
0013  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
0014  * version 3 of the license.                                                            *
0015  *                                                                                      *
0016  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0017  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0018  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0019  *                                                                                      *
0020  * You should have received a copy of the GNU General Public License along with         *
0021  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0022  ****************************************************************************************/
0023 
0024 #define DEBUG_PREFIX "Playlist::Model"
0025 
0026 #include "PlaylistModel.h"
0027 
0028 #include "core/support/Amarok.h"
0029 #include "SvgHandler.h"
0030 #include "amarokconfig.h"
0031 #include "AmarokMimeData.h"
0032 #include "core/capabilities/ReadLabelCapability.h"
0033 #include "core/support/Debug.h"
0034 #include "EngineController.h"
0035 #include "core/capabilities/MultiSourceCapability.h"
0036 #include "core/capabilities/SourceInfoCapability.h"
0037 #include "core/collections/Collection.h"
0038 #include "core/meta/Statistics.h"
0039 #include "core/meta/support/MetaUtility.h"
0040 #include "PlaylistDefines.h"
0041 #include "PlaylistActions.h"
0042 #include "PlaylistController.h"
0043 #include "PlaylistItem.h"
0044 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
0045 #include "core-impl/support/TrackLoader.h"
0046 #include "playlist/UndoCommands.h"
0047 
0048 #include <KLocalizedString>
0049 #include <KIconLoader>
0050 
0051 #include <QAction>
0052 #include <QTimer>
0053 #include <QDate>
0054 #include <QStringList>
0055 #include <QUrl>
0056 
0057 #include <algorithm>
0058 
0059 
0060 #define TOOLTIP_STATIC_LINEBREAK 50
0061 
0062 bool Playlist::Model::s_tooltipColumns[NUM_COLUMNS];
0063 bool Playlist::Model::s_showToolTip;
0064 
0065 // ------- helper functions for the tooltip
0066 
0067 static bool
0068 fitsInOneLineHTML(const QString& text)
0069 {
0070     // The size of the normal, standard line
0071     const int lnSize = TOOLTIP_STATIC_LINEBREAK;
0072     return (text.size() <= lnSize);
0073 }
0074 
0075 static QString
0076 breakLongLinesHTML( const QString &origText )
0077 {
0078     // filter-out HTML tags..
0079     QString text( origText );
0080     text.replace( '&', QLatin1String("&amp;") ); // needs to be first, obviously
0081     text.replace( '<', QLatin1String("&lt;") );
0082     text.replace( '>', QLatin1String("&gt;") );
0083 
0084     // Now let's break up long lines so that the tooltip doesn't become hideously large
0085     if( fitsInOneLineHTML( text ) )
0086         // If the text is not too long, return it as it is
0087         return text;
0088     else
0089     {
0090         const int lnSize = TOOLTIP_STATIC_LINEBREAK;
0091         QString textInLines;
0092 
0093         QStringList words = text.trimmed().split(' ');
0094         int lineLength = 0;
0095         while(words.size() > 0)
0096         {
0097             QString word = words.first();
0098             // Let's check if the next word makes the current line too long.
0099             if (lineLength + word.size() + 1 > lnSize)
0100             {
0101                 if (lineLength > 0)
0102                 {
0103                     textInLines += QLatin1String("<br/>");
0104                 }
0105                 lineLength = 0;
0106                 // Let's check if the next word is not too long for the new line to contain
0107                 // If it is, cut it
0108                 while (word.size() > lnSize)
0109                 {
0110                     QString wordPart = word;
0111                     wordPart.resize(lnSize);
0112                     word.remove(0,lnSize);
0113                     textInLines += wordPart + "<br/>";
0114                 }
0115             }
0116             textInLines += word + ' ';
0117             lineLength += word.size() + 1;
0118             words.removeFirst();
0119         }
0120         return textInLines.trimmed();
0121     }
0122 }
0123 
0124 /**
0125 * Prepares a row for the playlist tooltips consisting of an icon representing
0126 * an mp3 tag and its value
0127 * @param column The column used to display the icon
0128 * @param value The QString value to be shown
0129 * @param force If @c true, allows to set empty values
0130 * @return The line to be shown or an empty QString if the value is null
0131 */
0132 static QString
0133 HTMLLine( const Playlist::Column& column, const QString& value, bool force = false )
0134 {
0135     if( !value.isEmpty() || force )
0136     {
0137         QString line;
0138         line += QLatin1String("<tr><td align=\"right\">");
0139         line += "<img src=\""+KIconLoader::global()->iconPath( Playlist::iconName( column ), -16)+"\" />";
0140         line += QLatin1String("</td><td align=\"left\">");
0141         line += breakLongLinesHTML( value );
0142         line += QLatin1String("</td></tr>");
0143         return line;
0144     }
0145     else
0146         return QString();
0147 }
0148 
0149 /**
0150 * Prepares a row for the playlist tooltips consisting of an icon representing
0151 * an mp3 tag and its value
0152 * @param column The column used to display the icon
0153 * @param value The integer value to be shown
0154 * @param force If @c true, allows to set non-positive values
0155 * @return The line to be shown or an empty QString if the value is 0
0156 */
0157 static QString
0158 HTMLLine( const Playlist::Column& column, const int value, bool force = false )
0159 {
0160     // there is currently no numeric meta-data that would have sense if it were negative.
0161     // also, zero denotes not available, unknown etc; don't show these unless forced.
0162     if( value > 0 || force )
0163     {
0164         return HTMLLine( column, QString::number( value ) );
0165     }
0166     else
0167         return QString();
0168 }
0169 
0170 
0171 Playlist::Model::Model( QObject *parent )
0172         : QAbstractListModel( parent )
0173         , m_activeRow( -1 )
0174         , m_totalLength( 0 )
0175         , m_totalSize( 0 )
0176         , m_setStateOfItem_batchMinRow( -1 )
0177         , m_saveStateTimer( new QTimer(this) )
0178 {
0179     DEBUG_BLOCK
0180 
0181     m_saveStateTimer->setInterval( 5000 );
0182     m_saveStateTimer->setSingleShot( true );
0183     connect( m_saveStateTimer, &QTimer::timeout,
0184              this, &Playlist::Model::saveState );
0185     connect( this, &Playlist::Model::modelReset,
0186              this, &Playlist::Model::queueSaveState );
0187     connect( this, &Playlist::Model::dataChanged,
0188              this, &Playlist::Model::queueSaveState );
0189     connect( this, &Playlist::Model::rowsInserted,
0190              this, &Playlist::Model::queueSaveState );
0191     connect( this, &Playlist::Model::rowsMoved,
0192              this, &Playlist::Model::queueSaveState );
0193     connect( this, &Playlist::Model::rowsRemoved,
0194              this, &Playlist::Model::queueSaveState );
0195 }
0196 
0197 Playlist::Model::~Model()
0198 {
0199     DEBUG_BLOCK
0200 
0201     // Save current playlist
0202     exportPlaylist( Amarok::defaultPlaylistPath() );
0203 
0204     qDeleteAll( m_items );
0205 }
0206 
0207 void
0208 Playlist::Model::saveState()
0209 {
0210     exportPlaylist( Amarok::defaultPlaylistPath() );
0211 }
0212 
0213 void
0214 Playlist::Model::queueSaveState()
0215 {
0216     if ( !m_saveStateTimer->isActive() )
0217         m_saveStateTimer->start();
0218 }
0219 
0220 void
0221 Playlist::Model::insertTracksFromTrackLoader( const Meta::TrackList &tracks )
0222 {
0223     QObject *loader = sender();
0224     if( !sender() )
0225     {
0226         warning() << __PRETTY_FUNCTION__ << "can only be connected to TrackLoader";
0227         return;
0228     }
0229     int insertRow = loader->property( "beginRow" ).toInt();
0230     Controller::instance()->insertTracks( insertRow, tracks );
0231 }
0232 
0233 QVariant
0234 Playlist::Model::headerData( int section, Qt::Orientation orientation, int role ) const
0235 {
0236     Q_UNUSED( orientation );
0237 
0238     if ( role != Qt::DisplayRole )
0239         return QVariant();
0240 
0241     return columnName( static_cast<Playlist::Column>( section ) );
0242 }
0243 
0244 void
0245 Playlist::Model::setTooltipColumns( bool columns[] )
0246 {
0247     for( int i=0; i<Playlist::NUM_COLUMNS; ++i )
0248         s_tooltipColumns[i] = columns[i];
0249 }
0250 
0251 void
0252 Playlist::Model::enableToolTip( bool enable )
0253 {
0254     s_showToolTip = enable;
0255 }
0256 
0257 QString
0258 Playlist::Model::tooltipFor( Meta::TrackPtr track ) const
0259 {
0260     QString text;
0261     // get the shared pointers now to be thread safe
0262     Meta::ArtistPtr artist = track->artist();
0263     Meta::AlbumPtr album = track->album();
0264     Meta::ArtistPtr albumArtist = album ? album->albumArtist() : Meta::ArtistPtr();
0265     Meta::GenrePtr genre = track->genre();
0266     Meta::ComposerPtr composer = track->composer();
0267     Meta::YearPtr year = track->year();
0268     Meta::StatisticsPtr statistics = track->statistics();
0269 
0270     if( !track->isPlayable() )
0271         text += i18n( "<b>Note:</b> This track is not playable.<br>%1", track->notPlayableReason() );
0272 
0273     if( s_tooltipColumns[Playlist::Title] )
0274         text += HTMLLine( Playlist::Title, track->name() );
0275 
0276     if( s_tooltipColumns[Playlist::Artist] && artist )
0277         text += HTMLLine( Playlist::Artist, artist->name() );
0278 
0279     // only show albumArtist when different from artist (it should suffice to compare pointers)
0280     if( s_tooltipColumns[Playlist::AlbumArtist] && albumArtist && albumArtist != artist )
0281         text += HTMLLine( Playlist::AlbumArtist, albumArtist->name() );
0282 
0283     if( s_tooltipColumns[Playlist::Album] && album )
0284         text += HTMLLine( Playlist::Album, album->name() );
0285 
0286     if( s_tooltipColumns[Playlist::DiscNumber] )
0287         text += HTMLLine( Playlist::DiscNumber, track->discNumber() );
0288 
0289     if( s_tooltipColumns[Playlist::TrackNumber] )
0290         text += HTMLLine( Playlist::TrackNumber, track->trackNumber() );
0291 
0292     if( s_tooltipColumns[Playlist::Composer] && composer )
0293         text += HTMLLine( Playlist::Composer, composer->name() );
0294 
0295     if( s_tooltipColumns[Playlist::Genre] && genre )
0296         text += HTMLLine( Playlist::Genre, genre->name() );
0297 
0298     if( s_tooltipColumns[Playlist::Year] && year && year->year() > 0 )
0299         text += HTMLLine( Playlist::Year, year->year() );
0300 
0301     if( s_tooltipColumns[Playlist::Bpm] )
0302         text += HTMLLine( Playlist::Bpm, track->bpm() );
0303 
0304     if( s_tooltipColumns[Playlist::Comment]) {
0305         if ( !(fitsInOneLineHTML( track->comment() ) ) )
0306             text += HTMLLine( Playlist::Comment, i18n( "(...)" ) );
0307         else
0308             text += HTMLLine( Playlist::Comment, track->comment() );
0309     }
0310 
0311     if( s_tooltipColumns[Playlist::Labels] && !track->labels().empty() )
0312     {
0313         QStringList labels;
0314         foreach( Meta::LabelPtr label, track->labels() )
0315         {
0316             if( label )
0317                 labels << label->name();
0318         }
0319         text += HTMLLine( Playlist::Labels, labels.join( QStringLiteral(", ") ) );
0320     }
0321 
0322     if( s_tooltipColumns[Playlist::Score] )
0323         text += HTMLLine( Playlist::Score, statistics->score() );
0324 
0325     if( s_tooltipColumns[Playlist::Rating] )
0326         text += HTMLLine( Playlist::Rating, QString::number( statistics->rating()/2.0 ) );
0327 
0328     if( s_tooltipColumns[Playlist::PlayCount] )
0329         text += HTMLLine( Playlist::PlayCount, statistics->playCount(), true );
0330 
0331     if( s_tooltipColumns[Playlist::LastPlayed] && statistics->lastPlayed().isValid() )
0332         text += HTMLLine( Playlist::LastPlayed, QLocale().toString( statistics->lastPlayed() ) );
0333 
0334     if( s_tooltipColumns[Playlist::Bitrate] && track->bitrate() )
0335         text += HTMLLine( Playlist::Bitrate, i18nc( "%1: bitrate", "%1 kbps", track->bitrate() ) );
0336 
0337     if( text.isEmpty() )
0338         text = i18n( "No extra information available" );
0339     else
0340         text = QString("<table>"+ text +"</table>");
0341 
0342     return text;
0343 }
0344 
0345 QVariant
0346 Playlist::Model::data( const QModelIndex& index, int role ) const
0347 {
0348     int row = index.row();
0349 
0350     if ( !index.isValid() || !rowExists( row ) )
0351         return QVariant();
0352 
0353     if ( role == UniqueIdRole )
0354         return QVariant( idAt( row ) );
0355 
0356     else if ( role == ActiveTrackRole )
0357         return ( row == m_activeRow );
0358 
0359     else if ( role == TrackRole && m_items.at( row )->track() )
0360         return QVariant::fromValue( m_items.at( row )->track() );
0361 
0362     else if ( role == StateRole )
0363         return m_items.at( row )->state();
0364 
0365     else if ( role == QueuePositionRole )
0366         return Actions::instance()->queuePosition( idAt( row ) ) + 1;
0367 
0368     else if ( role == InCollectionRole )
0369         return  m_items.at( row )->track()->inCollection();
0370 
0371     else if ( role == MultiSourceRole )
0372         return  m_items.at( row )->track()->has<Capabilities::MultiSourceCapability>();
0373 
0374     else if ( role == StopAfterTrackRole )
0375         return Actions::instance()->willStopAfterTrack( idAt( row ) );
0376 
0377     else if ( role == Qt::ToolTipRole )
0378     {
0379         Meta::TrackPtr track = m_items.at( row )->track();
0380         if( s_showToolTip )
0381             return tooltipFor( track );
0382         else if( !track->isPlayable() )
0383             return i18n( "<b>Note:</b> This track is not playable.<br>%1", track->notPlayableReason() );
0384     }
0385 
0386     else if ( role == Qt::DisplayRole )
0387     {
0388         Meta::TrackPtr track = m_items.at( row )->track();
0389         Meta::AlbumPtr album = track->album();
0390         Meta::StatisticsPtr statistics = track->statistics();
0391         switch ( index.column() )
0392         {
0393             case PlaceHolder:
0394                 break;
0395             case Album:
0396             {
0397                 if( album )
0398                     return album->name();
0399                 break;
0400             }
0401             case AlbumArtist:
0402             {
0403                 if( album )
0404                 {
0405                     Meta::ArtistPtr artist = album->albumArtist();
0406                     if( artist )
0407                         return artist->name();
0408                 }
0409                 break;
0410             }
0411             case Artist:
0412             {
0413                 Meta::ArtistPtr artist = track->artist();
0414                 if( artist )
0415                     return artist->name();
0416                 break;
0417             }
0418             case Bitrate:
0419             {
0420                 return Meta::prettyBitrate( track->bitrate() );
0421             }
0422             case Bpm:
0423             {
0424                 if( track->bpm() > 0.0 )
0425                     return QString::number( track->bpm() );
0426                 break;
0427             }
0428             case Comment:
0429             {
0430                 return track->comment();
0431             }
0432             case Composer:
0433             {
0434                 Meta::ComposerPtr composer = track->composer();
0435                 if( composer )
0436                     return composer->name();
0437                 break;
0438             }
0439             case CoverImage:
0440             {
0441                 if( album )
0442                     return The::svgHandler()->imageWithBorder( album, 100 ); //FIXME:size?
0443                 break;
0444             }
0445             case Directory:
0446             {
0447                 if( track->playableUrl().isLocalFile() )
0448                     return track->playableUrl().adjusted(QUrl::RemoveFilename).path();
0449                 break;
0450             }
0451             case DiscNumber:
0452             {
0453                 if( track->discNumber() > 0 )
0454                     return track->discNumber();
0455                 break;
0456             }
0457             case Filename:
0458             {
0459 
0460                 if( track->playableUrl().isLocalFile() )
0461                     return track->playableUrl().fileName();
0462                 break;
0463             }
0464             case Filesize:
0465             {
0466                 return Meta::prettyFilesize( track->filesize() );
0467             }
0468             case Genre:
0469             {
0470                 Meta::GenrePtr genre = track->genre();
0471                 if( genre )
0472                     return genre->name();
0473                 break;
0474             }
0475             case GroupLength:
0476             {
0477                 return Meta::secToPrettyTime( 0 );
0478             }
0479             case GroupTracks:
0480             {
0481                 return QString();
0482             }
0483             case Labels:
0484             {
0485                 if( track )
0486                 {
0487                     QStringList labelNames;
0488                     foreach( const Meta::LabelPtr &label, track->labels() )
0489                     {
0490                         labelNames << label->prettyName();
0491                     }
0492                     return labelNames.join( QStringLiteral(", ") );
0493                 }
0494                 break;
0495             }
0496             case LastPlayed:
0497             {
0498                 if( statistics->playCount() == 0 )
0499                     return i18nc( "The amount of time since last played", "Never" );
0500                 else if( statistics->lastPlayed().isValid() )
0501                     return Amarok::verboseTimeSince( statistics->lastPlayed() );
0502                 else
0503                     return i18nc( "When this track was last played", "Unknown" );
0504             }
0505             case Length:
0506             {
0507                 return Meta::msToPrettyTime( track->length() );
0508             }
0509             case LengthInSeconds:
0510             {
0511                 return track->length() / 1000;
0512             }
0513             case Mood:
0514             {
0515                 return QString(); //FIXME
0516             }
0517             case PlayCount:
0518             {
0519                 return statistics->playCount();
0520             }
0521             case Rating:
0522             {
0523                 return statistics->rating();
0524             }
0525             case SampleRate:
0526             {
0527                 if( track->sampleRate() > 0 )
0528                     return track->sampleRate();
0529                 break;
0530             }
0531             case Score:
0532             {
0533                 return int( statistics->score() ); // Cast to int, as we don't need to show the decimals in the view..
0534             }
0535             case Source:
0536             {
0537                 QString sourceName;
0538                 Capabilities::SourceInfoCapability *sic = track->create<Capabilities::SourceInfoCapability>();
0539                 if ( sic )
0540                 {
0541                     sourceName = sic->sourceName();
0542                     delete sic;
0543                 }
0544                 else
0545                 {
0546                     sourceName = track->collection() ? track->collection()->prettyName() : QString();
0547                 }
0548                 return sourceName;
0549             }
0550             case SourceEmblem:
0551             {
0552                 QPixmap emblem;
0553                 Capabilities::SourceInfoCapability *sic = track->create<Capabilities::SourceInfoCapability>();
0554                 if ( sic )
0555                 {
0556                     QString source = sic->sourceName();
0557                     if ( !source.isEmpty() )
0558                         emblem = sic->emblem();
0559                     delete sic;
0560                 }
0561                 return emblem;
0562             }
0563             case Title:
0564             {
0565                 return track->prettyName();
0566             }
0567             case TitleWithTrackNum:
0568             {
0569                 QString trackString;
0570                 QString trackName = track->prettyName();
0571                 if( track->trackNumber() > 0 )
0572                 {
0573                     QString trackNumber = QString::number( track->trackNumber() );
0574                     trackString =  QString( trackNumber + " - " + trackName );
0575                 } else
0576                     trackString = trackName;
0577 
0578                 return trackString;
0579             }
0580             case TrackNumber:
0581             {
0582                 if( track->trackNumber() > 0 )
0583                     return track->trackNumber();
0584                 break;
0585             }
0586             case Type:
0587             {
0588                 return track->type();
0589             }
0590             case Year:
0591             {
0592                 Meta::YearPtr year = track->year();
0593                 if( year && year->year() > 0 )
0594                     return year->year();
0595                 break;
0596             }
0597             default:
0598                 return QVariant(); // returning a variant instead of a string inside a variant is cheaper
0599 
0600         }
0601     }
0602     // else
0603     return QVariant();
0604 }
0605 
0606 Qt::DropActions
0607 Playlist::Model::supportedDropActions() const
0608 {
0609     return Qt::MoveAction | QAbstractListModel::supportedDropActions();
0610 }
0611 
0612 Qt::ItemFlags
0613 Playlist::Model::flags( const QModelIndex &index ) const
0614 {
0615     if ( index.isValid() )
0616         return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable );
0617     return Qt::ItemIsDropEnabled;
0618 }
0619 
0620 QStringList
0621 Playlist::Model::mimeTypes() const
0622 {
0623     QStringList ret = QAbstractListModel::mimeTypes();
0624     ret << AmarokMimeData::TRACK_MIME;
0625     ret << QStringLiteral("text/uri-list"); //we do accept urls
0626     return ret;
0627 }
0628 
0629 QMimeData*
0630 Playlist::Model::mimeData( const QModelIndexList &indexes ) const
0631 {
0632     AmarokMimeData* mime = new AmarokMimeData();
0633     Meta::TrackList selectedTracks;
0634 
0635     foreach( const QModelIndex &it, indexes )
0636     selectedTracks << m_items.at( it.row() )->track();
0637 
0638     mime->setTracks( selectedTracks );
0639     return mime;
0640 }
0641 
0642 bool
0643 Playlist::Model::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex &parent )
0644 {
0645     if ( action == Qt::IgnoreAction )
0646         return true;
0647 
0648     int beginRow;
0649     if ( row != -1 )
0650         beginRow = row;
0651     else if ( parent.isValid() )
0652         beginRow = parent.row();
0653     else
0654         beginRow = m_items.size();
0655 
0656     if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
0657     {
0658         debug() << "this is a track";
0659         const AmarokMimeData* trackListDrag = qobject_cast<const AmarokMimeData*>( data );
0660         if( trackListDrag )
0661         {
0662 
0663             Meta::TrackList tracks = trackListDrag->tracks();
0664             std::stable_sort( tracks.begin(), tracks.end(), Meta::Track::lessThan );
0665 
0666             The::playlistController()->insertTracks( beginRow, tracks );
0667         }
0668         return true;
0669     }
0670     else if( data->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
0671     {
0672         debug() << "this is a playlist";
0673         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
0674         if( dragList )
0675             The::playlistController()->insertPlaylists( beginRow, dragList->playlists() );
0676         return true;
0677     }
0678     else if( data->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) )
0679     {
0680         debug() << "this is a podcast episode";
0681         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
0682         if( dragList )
0683         {
0684             Meta::TrackList tracks;
0685             foreach( Podcasts::PodcastEpisodePtr episode, dragList->podcastEpisodes() )
0686                 tracks << Meta::TrackPtr::staticCast( episode );
0687             The::playlistController()->insertTracks( beginRow, tracks );
0688         }
0689         return true;
0690     }
0691     else if( data->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
0692     {
0693         debug() << "this is a podcast channel";
0694         const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
0695         if( dragList )
0696         {
0697             Meta::TrackList tracks;
0698             foreach( Podcasts::PodcastChannelPtr channel, dragList->podcastChannels() )
0699                 foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
0700                     tracks << Meta::TrackPtr::staticCast( episode );
0701             The::playlistController()->insertTracks( beginRow, tracks );
0702         }
0703         return true;
0704     }
0705     else if( data->hasUrls() )
0706     {
0707         debug() << "this is _something_ with a url....";
0708         TrackLoader *dl = new TrackLoader(); // auto-deletes itself
0709         dl->setProperty( "beginRow", beginRow );
0710         connect( dl, &TrackLoader::finished, this, &Model::insertTracksFromTrackLoader );
0711         dl->init( data->urls() );
0712         return true;
0713     }
0714 
0715     debug() << "I have no idea what the hell this is...";
0716     return false;
0717 }
0718 
0719 void
0720 Playlist::Model::setActiveRow( int row )
0721 {
0722     if ( rowExists( row ) )
0723     {
0724         setStateOfRow( row, Item::Played );
0725         m_activeRow = row;
0726         Q_EMIT activeTrackChanged( m_items.at( row )->id() );
0727     }
0728     else
0729     {
0730         m_activeRow = -1;
0731         Q_EMIT activeTrackChanged( 0 );
0732     }
0733 }
0734 
0735 void
0736 Playlist::Model::emitQueueChanged()
0737 {
0738     Q_EMIT queueChanged();
0739 }
0740 
0741 int
0742 Playlist::Model::queuePositionOfRow( int row )
0743 {
0744     return Actions::instance()->queuePosition( idAt( row ) ) + 1;
0745 }
0746 
0747 Playlist::Item::State
0748 Playlist::Model::stateOfRow( int row ) const
0749 {
0750     if ( rowExists( row ) )
0751         return m_items.at( row )->state();
0752     else
0753         return Item::Invalid;
0754 }
0755 
0756 bool
0757 Playlist::Model::containsTrack( const Meta::TrackPtr& track ) const
0758 {
0759     foreach( Item* i, m_items )
0760     {
0761         if ( i->track() == track )
0762             return true;
0763     }
0764     return false;
0765 }
0766 
0767 int
0768 Playlist::Model::firstRowForTrack( const Meta::TrackPtr& track ) const
0769 {
0770     int row = 0;
0771     foreach( Item* i, m_items )
0772     {
0773         if ( i->track() == track )
0774             return row;
0775         row++;
0776     }
0777     return -1;
0778 }
0779 
0780 QSet<int>
0781 Playlist::Model::allRowsForTrack( const Meta::TrackPtr& track ) const
0782 {
0783     QSet<int> trackRows;
0784 
0785     int row = 0;
0786     foreach( Item* i, m_items )
0787     {
0788         if ( i->track() == track )
0789             trackRows.insert( row );
0790         row++;
0791     }
0792     return trackRows;
0793 }
0794 
0795 Meta::TrackPtr
0796 Playlist::Model::trackAt( int row ) const
0797 {
0798     if ( rowExists( row ) )
0799         return m_items.at( row )->track();
0800     else
0801         return Meta::TrackPtr();
0802 }
0803 
0804 Meta::TrackPtr
0805 Playlist::Model::activeTrack() const
0806 {
0807     if ( rowExists( m_activeRow ) )
0808         return m_items.at( m_activeRow )->track();
0809     else
0810         return Meta::TrackPtr();
0811 }
0812 
0813 int
0814 Playlist::Model::rowForId( const quint64 id ) const
0815 {
0816     return m_items.indexOf( m_itemIds.value( id ) );    // Returns -1 on miss, same as our API.
0817 }
0818 
0819 Meta::TrackPtr
0820 Playlist::Model::trackForId( const quint64 id ) const
0821 {
0822     Item* item = m_itemIds.value( id, nullptr );
0823     if ( item )
0824         return item->track();
0825     else
0826         return Meta::TrackPtr();
0827 }
0828 
0829 quint64
0830 Playlist::Model::idAt( const int row ) const
0831 {
0832     if ( rowExists( row ) )
0833         return m_items.at( row )->id();
0834     else
0835         return 0;
0836 }
0837 
0838 quint64
0839 Playlist::Model::activeId() const
0840 {
0841     if ( rowExists( m_activeRow ) )
0842         return m_items.at( m_activeRow )->id();
0843     else
0844         return 0;
0845 }
0846 
0847 Playlist::Item::State
0848 Playlist::Model::stateOfId( quint64 id ) const
0849 {
0850     Item* item = m_itemIds.value( id, nullptr );
0851     if ( item )
0852         return item->state();
0853     else
0854         return Item::Invalid;
0855 }
0856 
0857 void
0858 Playlist::Model::metadataChanged(const Meta::TrackPtr &track )
0859 {
0860     int row = 0;
0861     foreach( Item* i, m_items )
0862     {
0863         if ( i->track() == track )
0864         {
0865             // ensure that we really have the correct album subscribed (in case it changed)
0866             Meta::AlbumPtr album = track->album();
0867             if( album )
0868                 subscribeTo( album );
0869 
0870             Q_EMIT dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
0871         }
0872         row++;
0873     }
0874 }
0875 
0876 void
0877 Playlist::Model::metadataChanged(const Meta::AlbumPtr &album )
0878 {
0879     // Mainly to get update about changed covers
0880 
0881     // -- search for all the tracks having this album
0882     bool found = false;
0883     const int size = m_items.size();
0884     for ( int i = 0; i < size; i++ )
0885     {
0886         if ( m_items.at( i )->track()->album() == album )
0887         {
0888             Q_EMIT dataChanged( index( i, 0 ), index( i, columnCount() - 1 ) );
0889             found = true;
0890             debug()<<"Metadata updated for album"<<album->prettyName();
0891         }
0892     }
0893 
0894     // -- unsubscribe if we don't have a track from that album left.
0895     // this can happen if the album of a track changed
0896     if( !found )
0897         unsubscribeFrom( album );
0898 }
0899 
0900 bool
0901 Playlist::Model::exportPlaylist( const QString &path, bool relative )
0902 {
0903     // check queue state
0904     QQueue<quint64> queueIds = The::playlistActions()->queue();
0905     QList<int> queued;
0906     foreach( quint64 id, queueIds ) {
0907       queued << rowForId( id );
0908     }
0909     return Playlists::exportPlaylistFile( tracks(), QUrl::fromLocalFile(path), relative, queued);
0910 }
0911 
0912 Meta::TrackList
0913 Playlist::Model::tracks()
0914 {
0915     Meta::TrackList tl;
0916     foreach( Item* item, m_items )
0917         tl << item->track();
0918     return tl;
0919 }
0920 
0921 QString
0922 Playlist::Model::prettyColumnName( Column index ) //static
0923 {
0924     switch ( index )
0925     {
0926     case Filename:   return i18nc( "The name of the file this track is stored in", "Filename" );
0927     case Title:      return i18n( "Title" );
0928     case Artist:     return i18n( "Artist" );
0929     case AlbumArtist: return i18n( "Album Artist" );
0930     case Composer:   return i18n( "Composer" );
0931     case Year:       return i18n( "Year" );
0932     case Album:      return i18n( "Album" );
0933     case DiscNumber: return i18n( "Disc Number" );
0934     case TrackNumber: return i18nc( "The Track number for this item", "Track" );
0935     case Bpm:        return i18n( "BPM" );
0936     case Genre:      return i18n( "Genre" );
0937     case Comment:    return i18n( "Comment" );
0938     case Directory:  return i18nc( "The location on disc of this track", "Directory" );
0939     case Type:       return i18n( "Type" );
0940     case Length:     return i18n( "Length" );
0941     case Bitrate:    return i18n( "Bitrate" );
0942     case SampleRate: return i18n( "Sample Rate" );
0943     case Score:      return i18n( "Score" );
0944     case Rating:     return i18n( "Rating" );
0945     case PlayCount:  return i18n( "Play Count" );
0946     case LastPlayed: return i18nc( "Column name", "Last Played" );
0947     case Mood:       return i18n( "Mood" );
0948     case Filesize:   return i18n( "File Size" );
0949     default:         return QStringLiteral("This is a bug.");
0950     }
0951 
0952 }
0953 
0954 
0955 ////////////
0956 //Private Methods
0957 ////////////
0958 
0959 void
0960 Playlist::Model::insertTracksCommand( const InsertCmdList& cmds )
0961 {
0962     if ( cmds.size() < 1 )
0963         return;
0964 
0965     setAllNewlyAddedToUnplayed();
0966 
0967     int activeShift = 0;
0968     int min = m_items.size() + cmds.size();
0969     int max = 0;
0970     int begin = cmds.at( 0 ).second;
0971     foreach( const InsertCmd &ic, cmds )
0972     {
0973         min = qMin( min, ic.second );
0974         max = qMax( max, ic.second );
0975         activeShift += ( begin <= m_activeRow ) ? 1 : 0;
0976     }
0977 
0978     // actually do the insertion
0979     beginInsertRows( QModelIndex(), min, max );
0980     foreach( const InsertCmd &ic, cmds )
0981     {
0982         Meta::TrackPtr track = ic.first;
0983         m_totalLength += track->length();
0984         m_totalSize += track->filesize();
0985         subscribeTo( track );
0986         Meta::AlbumPtr album = track->album();
0987         if( album )
0988             subscribeTo( album );
0989 
0990         Item* newitem = new Item( track );
0991         m_items.insert( ic.second, newitem );
0992         m_itemIds.insert( newitem->id(), newitem );
0993     }
0994     endInsertRows();
0995 
0996     if( m_activeRow >= 0 )
0997         m_activeRow += activeShift;
0998     else
0999     {
1000         EngineController *engine = The::engineController();
1001         if( engine ) // test cases might create a playlist without having an EngineController
1002         {
1003             const Meta::TrackPtr engineTrack = engine->currentTrack();
1004             if( engineTrack )
1005             {
1006                 int engineRow = firstRowForTrack( engineTrack );
1007                 if( engineRow > -1 )
1008                     setActiveRow( engineRow );
1009             }
1010         }
1011     }
1012 }
1013 
1014 static bool
1015 removeCmdLessThanByRow( const Playlist::RemoveCmd &left, const Playlist::RemoveCmd &right )
1016 {
1017     return left.second < right.second;
1018 }
1019 
1020 void
1021 Playlist::Model::removeTracksCommand( const RemoveCmdList &passedCmds )
1022 {
1023     DEBUG_BLOCK
1024     if ( passedCmds.size() < 1 )
1025         return;
1026 
1027     if ( passedCmds.size() == m_items.size() )
1028     {
1029         clearCommand();
1030         return;
1031     }
1032 
1033     // sort tracks to remove by their row
1034     RemoveCmdList cmds( passedCmds );
1035     std::sort( cmds.begin(), cmds.end(), removeCmdLessThanByRow );
1036 
1037     // update the active row
1038     if( m_activeRow >= 0 )
1039     {
1040         int activeShift = 0;
1041         foreach( const RemoveCmd &rc, cmds )
1042         {
1043             if( rc.second < m_activeRow )
1044                 activeShift++;
1045             else if( rc.second == m_activeRow )
1046                 m_activeRow = -1; // disappeared
1047             else
1048                 break; // we got over it, nothing left to do
1049         }
1050         if( m_activeRow >= 0 ) // not deleted
1051             m_activeRow -= activeShift;
1052     }
1053 
1054     QSet<Meta::TrackPtr> trackUnsubscribeCandidates;
1055     QSet<Meta::AlbumPtr> albumUnsubscribeCandidates;
1056 
1057     QListIterator<RemoveCmd> it( cmds );
1058     int removedRows = 0;
1059     while( it.hasNext() )
1060     {
1061         int startRow = it.next().second;
1062         int endRow = startRow;
1063 
1064         // find consecutive runs of rows, this is important to group begin/endRemoveRows(),
1065         // which are very costly when there are many proxymodels and a view above.
1066         while( it.hasNext() && it.peekNext().second == endRow + 1 )
1067         {
1068             it.next();
1069             endRow++;
1070         }
1071 
1072         beginRemoveRows( QModelIndex(), startRow - removedRows, endRow - removedRows );
1073         for( int row = startRow; row <= endRow; row++ )
1074         {
1075             Item *removedItem = m_items.at( row - removedRows );
1076             m_items.removeAt( row - removedRows );
1077             m_itemIds.remove( removedItem->id() );
1078 
1079             const Meta::TrackPtr &track = removedItem->track();
1080             // update totals here so they're right when endRemoveRows() called
1081             m_totalLength -= track->length();
1082             m_totalSize -= track->filesize();
1083             trackUnsubscribeCandidates.insert( track );
1084             Meta::AlbumPtr album = track->album();
1085             if( album )
1086                 albumUnsubscribeCandidates.insert( album );
1087 
1088             delete removedItem; // note track is by reference, needs removedItem alive
1089             removedRows++;
1090         }
1091         endRemoveRows();
1092     }
1093 
1094     // unsubscribe from tracks no longer present in playlist
1095     foreach( Meta::TrackPtr track, trackUnsubscribeCandidates )
1096     {
1097         if( !containsTrack( track ) )
1098             unsubscribeFrom( track );
1099     }
1100 
1101     // unsubscribe from albums no longer present im playlist
1102     QSet<Meta::AlbumPtr> remainingAlbums;
1103     foreach( const Item *item, m_items )
1104     {
1105         Meta::AlbumPtr album = item->track()->album();
1106         if( album )
1107             remainingAlbums.insert( album );
1108     }
1109     foreach( Meta::AlbumPtr album, albumUnsubscribeCandidates )
1110     {
1111         if( !remainingAlbums.contains( album ) )
1112             unsubscribeFrom( album );
1113     }
1114 
1115     // make sure that there are enough tracks if we just removed from a dynamic playlist.
1116     // This call needs to be delayed or else we would mess up the undo queue
1117     // BUG: 259675
1118     // FIXME: removing the track and normalizing the playlist should be grouped together
1119     //        so that an undo operation undos both.
1120     QTimer::singleShot(0, Playlist::Actions::instance(), &Playlist::Actions::normalizeDynamicPlaylist);
1121 }
1122 
1123 
1124 void Playlist::Model::clearCommand()
1125 {
1126     setActiveRow( -1 );
1127 
1128     beginRemoveRows( QModelIndex(), 0, rowCount() - 1 );
1129 
1130     m_totalLength = 0;
1131     m_totalSize = 0;
1132 
1133     qDeleteAll( m_items );
1134     m_items.clear();
1135     m_itemIds.clear();
1136 
1137     endRemoveRows();
1138 }
1139 
1140 
1141 // Note: this function depends on 'MoveCmdList' to be a complete "cycle", in the sense
1142 // that if row A is moved to row B, another row MUST be moved to row A.
1143 // Very strange API design IMHO, because it forces our caller to e.g. move ALL ROWS in
1144 // the playlist to move row 0 to the last row. This function should just have been
1145 // equivalent to a 'removeTracks()' followed by an 'insertTracks()' IMHO.  --Nanno
1146 
1147 void
1148 Playlist::Model::moveTracksCommand( const MoveCmdList& cmds, bool reverse )
1149 {
1150     DEBUG_BLOCK
1151     debug()<<"moveTracksCommand:"<<cmds.size()<<reverse;
1152 
1153     if ( cmds.size() < 1 )
1154         return;
1155 
1156     int min = INT_MAX;
1157     int max = INT_MIN;
1158     foreach( const MoveCmd &rc, cmds )
1159     {
1160         min = qMin( min, rc.first );
1161         max = qMax( max, rc.first );
1162     }
1163 
1164     if( min < 0 || max >= m_items.size() )
1165     {
1166         error() << "Wrong row numbers given";
1167         return;
1168     }
1169 
1170     int newActiveRow = m_activeRow;
1171     QList<Item*> oldItems( m_items );
1172     if ( reverse )
1173     {
1174         foreach( const MoveCmd &mc, cmds )
1175         {
1176             m_items[mc.first] = oldItems.at( mc.second );
1177             if ( m_activeRow == mc.second )
1178                 newActiveRow = mc.first;
1179         }
1180     }
1181     else
1182     {
1183         foreach( const MoveCmd &mc, cmds )
1184         {
1185             m_items[mc.second] = oldItems.at( mc.first );
1186             if ( m_activeRow == mc.first )
1187                 newActiveRow = mc.second;
1188         }
1189     }
1190 
1191     // We have 3 choices:
1192     //   - Call 'beginMoveRows()' / 'endMoveRows()'. Drawback: we'd need to do N of them, all causing resorts etc.
1193     //   - Emit 'layoutAboutToChange' / 'layoutChanged'. Drawback: unspecific, 'changePersistentIndex()' complications.
1194     //   - Emit 'dataChanged'. Drawback: a bit inappropriate. But not wrong.
1195     Q_EMIT dataChanged( index( min, 0 ), index( max, columnCount() - 1 ) );
1196 
1197     //update the active row
1198     m_activeRow = newActiveRow;
1199 }
1200 
1201 
1202 // When doing a 'setStateOfItem_batch', we Q_EMIT 1 crude 'dataChanged' signal. If we're
1203 // unlucky, that signal may span many innocent rows that haven't changed at all.
1204 // Although that "worst case" will cause unnecessary work in our listeners "upstream", it
1205 // still has much better performance than the worst case of emitting very many tiny
1206 // 'dataChanged' signals.
1207 //
1208 // Being more clever (coalesce multiple contiguous ranges, etc.) is not worth the effort.
1209 void
1210 Playlist::Model::setStateOfItem_batchStart()
1211 {
1212     m_setStateOfItem_batchMinRow = rowCount() + 1;
1213     m_setStateOfItem_batchMaxRow = 0;
1214 }
1215 
1216 void
1217 Playlist::Model::setStateOfItem_batchEnd()
1218 {
1219     if ( ( m_setStateOfItem_batchMaxRow - m_setStateOfItem_batchMinRow ) >= 0 )
1220         Q_EMIT dataChanged( index( m_setStateOfItem_batchMinRow, 0 ), index( m_setStateOfItem_batchMaxRow, columnCount() - 1 ) );
1221 
1222     m_setStateOfItem_batchMinRow = -1;
1223 }
1224 
1225 void
1226 Playlist::Model::setStateOfItem( Item *item, int row, Item::State state )
1227 {
1228     item->setState( state );
1229 
1230     if ( m_setStateOfItem_batchMinRow == -1 )    // If not in batch mode
1231         Q_EMIT dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
1232     else
1233     {
1234         m_setStateOfItem_batchMinRow = qMin( m_setStateOfItem_batchMinRow, row );
1235         m_setStateOfItem_batchMaxRow = qMax( m_setStateOfItem_batchMaxRow, row );
1236     }
1237 }
1238 
1239 
1240 // Unimportant TODO: the performance of this function is O(n) in playlist size.
1241 // Not a big problem, because it's called infrequently.
1242 // Can be fixed by maintaining a new member variable 'QMultiHash<Item::State, Item*>'.
1243 void
1244 Playlist::Model::setAllNewlyAddedToUnplayed()
1245 {
1246     DEBUG_BLOCK
1247 
1248     setStateOfItem_batchStart();
1249 
1250     for ( int row = 0; row < rowCount(); row++ )
1251     {
1252         Item* item = m_items.at( row );
1253         if ( item->state() == Item::NewlyAdded )
1254             setStateOfItem( item, row, Item::Unplayed );
1255     }
1256 
1257     setStateOfItem_batchEnd();
1258 }
1259 
1260 void Playlist::Model::setAllUnplayed()
1261 {
1262     DEBUG_BLOCK
1263 
1264     setStateOfItem_batchStart();
1265 
1266     for ( int row = 0; row < rowCount(); row++ )
1267     {
1268         Item* item = m_items.at( row );
1269         setStateOfItem( item, row, Item::Unplayed );
1270     }
1271 
1272     setStateOfItem_batchEnd();
1273 }