Warning, file /multimedia/amarok/src/playlist/PlaylistModel.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
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("&") ); // needs to be first, obviously 0081 text.replace( '<', QLatin1String("<") ); 0082 text.replace( '>', QLatin1String(">") ); 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 }